package de.blau.android.osm;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import org.xmlpull.v1.XmlSerializer;
import android.content.Context;
import android.content.res.Resources;
import android.support.annotation.Nullable;
import de.blau.android.App;
import de.blau.android.R;
import de.blau.android.presets.Preset;
import de.blau.android.presets.Preset.PresetItem;
import de.blau.android.util.IssueAlert;
public abstract class OsmElement implements Serializable, XmlSerializable, JosmXmlSerializable {
/**
*
*/
private static final long serialVersionUID = 7711945069147743670L;
public static final long NEW_OSM_ID = -1;
public static final byte STATE_UNCHANGED = 0;
public static final byte STATE_CREATED = 1;
public static final byte STATE_MODIFIED = 2;
public static final byte STATE_DELETED = 3;
long osmId;
long osmVersion;
TreeMap<String, String> tags;
byte state;
ArrayList<Relation> parentRelations;
/**
* hasProblem() is an expensive test, so the results are cached.
* old version used a Boolean object which was silly we could naturally encode these as bits
*/
private boolean cachedHasProblem = false;
private boolean checkedForProblem = false; // flag indicating if cachedHasProblem is valid
OsmElement(final long osmId, final long osmVersion, final byte state) {
this.osmId = osmId;
this.osmVersion = osmVersion;
this.tags = null;
this.state = state;
this.parentRelations = null;
}
public long getOsmId() {
return osmId;
}
public long getOsmVersion() {
return osmVersion;
}
void setOsmVersion(long osmVersion) {
this.osmVersion = osmVersion;
}
void setOsmId(final long osmId) {
this.osmId = osmId;
}
public SortedMap<String,String> getTags() {
if (tags == null) {
return Collections.unmodifiableSortedMap(new TreeMap<String, String>()); // for backwards compatibility
}
return Collections.unmodifiableSortedMap(tags);
}
/**
* @return true if the element has at least one tag
*/
public boolean hasTags() {
return tags != null && tags.size() > 0;
}
public byte getState() {
return state;
}
/** gives a string description of the element type (e.g. 'node', 'way' or 'relation') - see also {@link #getType()} is rather confusingly named */
abstract public String getName();
/**
* Does not set the state if it's on CREATED, but if new state is DELETED.
*
* @param newState
*/
void updateState(final byte newState) {
if (state != STATE_CREATED || newState == STATE_DELETED) {
state = newState;
}
}
void setState(final byte newState) {
state = newState;
}
void addOrUpdateTag(final String tag, final String value) {
if (tags==null) {
tags = new TreeMap<String, String>();
}
tags.put(tag, value);
checkedForProblem = false;
}
/**
* Add the tags of the element, replacing any existing tags.
* @param tags New tags to add or to replace existing tags.
*/
void addTags(final Map<String, String> tags) {
if (tags != null) {
if (this.tags==null) {
this.tags = new TreeMap<String, String>();
}
this.tags.putAll(tags);
checkedForProblem = false;
}
}
/**
* Set the tags of the element, replacing all existing tags.
* @param tags New tags to replace existing tags.
* @return Flag indicating if the tags have actually changed.
*/
boolean setTags(@Nullable final Map<String, String> tags) {
if (this.tags == null) {
addTags(tags);
return true;
} else if (!this.tags.equals(tags)) {
this.tags.clear();
addTags(tags);
return true;
}
return false;
}
/**
* @param key the key to search for (case sensitive)
* @param value the value to search for (case sensitive)
* @return true if the element has a tag with this key and value.
*/
public boolean hasTag(final String key, final String value) {
if (tags == null) {
return false;
}
String keyValue = tags.get(key);
return keyValue != null && keyValue.equals(value);
}
/**
* @param tags tags to use instead of the standard ones
* @param key the key to search for (case sensitive)
* @param value the value to search for (case sensitive)
* @return true if the element has a tag with this key and value.
*/
boolean hasTag(final Map<String, String> tags, final String key, final String value) {
if (tags == null) {
return false;
}
String keyValue = tags.get(key);
return keyValue != null && keyValue.equals(value);
}
/**
* @param key the key to search for (case sensitive)
* @return the value of this key.
*/
public String getTagWithKey(final String key) {
if (tags == null) {
return null;
}
return tags.get(key);
}
/**
* @param key the key to search for (case sensitive)
* @return true if the element has a tag with this key.
*/
public boolean hasTagKey(final String key) {
return getTagWithKey(key) != null;
}
/**
* check if this element has tags of any kind
* @return
*/
public boolean isTagged() {
return (tags != null) && (tags.size() > 0);
}
/**
* Merge the tags from two OsmElements into one set.
* @param e1
* @param e2
* @return
*/
public static Map<String, String> mergedTags(OsmElement e1, OsmElement e2) {
Map<String, String> merged = new TreeMap<String, String>(e1.getTags());
Map<String, String> fromTags = e2.getTags();
for (String key : fromTags.keySet()) {
Set<String> values = new HashSet<String>(Arrays.asList(fromTags.get(key).split("\\;")));
if (merged.containsKey(key)) {
values.addAll(Arrays.asList(merged.get(key).split("\\;")));
}
StringBuilder b = new StringBuilder();
for (String v : values) {
if (b.length() > 0) b.append(';');
b.append(v);
}
merged.put(key, b.toString());
}
return merged;
}
@Override
public String toString() {
return getName() + " " + osmId;
}
void tagsToXml(final XmlSerializer s) throws IllegalArgumentException,
IllegalStateException, IOException {
if (tags != null) {
for (Entry<String, String> tag : tags.entrySet()) {
s.startTag("", "tag");
s.attribute("", "k", tag.getKey());
s.attribute("", "v", tag.getValue());
s.endTag("", "tag");
}
}
}
public boolean isUnchanged() {
return state == STATE_UNCHANGED;
}
/**
* Add reference to parent relation
* Does not check id to avoid dups!
*/
public void addParentRelation(Relation relation) {
if (parentRelations == null) {
parentRelations = new ArrayList<Relation>();
}
parentRelations.add(relation);
}
/**
* Check for parent relation
* @param relation
* @return
*/
public boolean hasParentRelation(Relation relation) {
return (parentRelations != null && parentRelations.contains(relation));
}
/**
* Check for parent relation based on id
* @param relation
* @return
*/
public boolean hasParentRelation(long osmId) {
if (parentRelations == null) {
return false;
}
for (Relation r:parentRelations) {
if (osmId == r.getOsmId())
return true;
}
return false;
}
/**
* Add all parent relations, avoids dups
*/
public void addParentRelations(ArrayList<Relation> relations) {
if (parentRelations == null) {
parentRelations = new ArrayList<Relation>();
}
// dedup
for (Relation r : relations) {
if (!parentRelations.contains(r)) {
addParentRelation(r);
}
}
}
public ArrayList<Relation> getParentRelations() {
return parentRelations;
}
public boolean hasParentRelations() {
return (parentRelations != null) && (parentRelations.size() > 0);
}
/**
* Remove reference to parent relation
* does not check for id
*/
public void removeParentRelation(Relation relation) {
if (parentRelations != null) {
parentRelations.remove(relation);
}
}
/**
* Remove reference to parent relation
*/
public void removeParentRelation(long osmId) {
if (parentRelations != null) {
ArrayList<Relation> tempRelList = new ArrayList<Relation>(parentRelations);
for (Relation r:tempRelList) {
if (osmId == r.getOsmId())
parentRelations.remove(r);
}
}
}
/**
* Generate a human-readable description/summary of the element.
* @return A description of the element.
*/
public String getDescription() {
return getDescription(true);
}
/**
* Generate a human-readable description/summary of the element.
* @return A description of the element.
*/
public String getDescription(Context ctx) {
return getDescription(ctx, true);
}
/**
* Return a concise description of the element
* @param withType
* @return
*/
public String getDescription(boolean withType) {
return getDescription(null, withType);
}
/**
* Return a concise description of the element
* @param withType
* @return
*/
private String getDescription(Context ctx, boolean withType) {
// Use the name if it exists
String name = getTagWithKey(Tags.KEY_NAME);
if (name != null && name.length() > 0) {
return name;
}
// Then the address
String housenumber = getTagWithKey(Tags.KEY_ADDR_HOUSENUMBER);
if (housenumber != null && housenumber.length() > 0) {
try {
String street = getTagWithKey(Tags.KEY_ADDR_STREET);
if (street != null && street.length() > 0) {
if (ctx != null) {
return ctx.getResources().getString(R.string.address_housenumber_street, street, housenumber);
} else {
return "address " + housenumber + " " + street;
}
} else {
if (ctx != null) {
return ctx.getResources().getString(R.string.address_housenumber, housenumber);
} else {
return "address " + housenumber;
}
}
} catch (Exception ex) {
// protect against translation errors
}
}
// try to match with a preset
if (ctx != null) {
PresetItem p = Preset.findBestMatch(App.getCurrentPresets(ctx),tags);
if (p!=null) {
String ref = getTagWithKey(Tags.KEY_REF);
return p.getTranslatedName() + (ref != null ? " " + ref : "") ;
}
}
// Then the value of the most 'important' tag the element has
String tag = getPrimaryTag();
if (tag != null) {
return (withType ? getName() + " " : "") + tag;
}
// Failing the above, the OSM ID
return (withType ? getName() + " #" : "#") + Long.toString(getOsmId());
}
/**
* @return the first kay =value of any important tags or null if none found
*/
public String getPrimaryTag() {
for (String tag : Tags.IMPORTANT_TAGS) {
String value = getTagWithKey(tag);
if (value != null && value.length() > 0) {
return tag + "=" + value;
}
}
return null;
}
/**
* Generate a description of the element that also includes state information.
* @param aResources Application resources.
* @return A human readable description of the element that includes state information.
*/
public String getStateDescription(final Resources aResources) {
int resid;
switch (getState()) {
case STATE_CREATED:
resid = R.string.changes_created;
break;
case STATE_MODIFIED:
resid = R.string.changes_changed;
break;
case STATE_DELETED:
resid = R.string.changes_deleted;
break;
default:
resid = 0;
break;
}
String result = getDescription();
if (resid != 0) {
result = aResources.getString(resid, result);
}
return result;
}
/**
* Test if the element has any problems by searching all the tags for the words
* "fixme" or "todo".
* @return true if the element has any noted problems, false otherwise.
*/
boolean calcProblem() {
final String pattern = "(?i).*\\b(?:fixme|todo)\\b.*";
if (tags != null) {
for (String key : tags.keySet()) {
// test key and value against pattern
if (key.matches(pattern) || tags.get(key).matches(pattern)) {
return true;
}
}
}
return false;
}
/**
* return a string giving the problem
*/
public String describeProblem() {
final String pattern = "(?i).*\\b(?:fixme|todo)\\b.*";
if (tags != null) {
for (String key : tags.keySet()) {
// test key and value against pattern
if (key.matches(pattern) || tags.get(key).matches(pattern)) {
return key + ": " + tags.get(key);
}
}
}
return "";
}
/**
* Test if the element has a noted problem. A noted problem is where someone has
* tagged the element with a "fixme" or "todo" key/value.
* @return true if the element has a noted problem, false if it doesn't.
*/
public boolean hasProblem(Context context) {
// This implementation assumes that calcProblem() may be expensive, and
// caches the calculation.
if (!checkedForProblem) {
checkedForProblem = true; // don't re-check
cachedHasProblem = calcProblem();
if (cachedHasProblem && context != null) {
IssueAlert.alert(context, this);
}
}
return cachedHasProblem;
}
/**
* Call if you have made a change that potentially changes the problem state of the element
*/
public void resetHasProblem() {
checkedForProblem = false;
}
/** (see also {@link #getName()} - this returns the full type, differentiating between open and closed ways)
* @return the {@link ElementType} of the element */
public abstract ElementType getType();
/**
* Version of above that uses a potential different set of tags
* @param tags
* @return
*/
public abstract ElementType getType(Map<String, String>tags);
/** Enum for element types (Node, Way, Closed Ways, Relations, Areas (MPs) */
public enum ElementType {
NODE,
WAY,
CLOSEDWAY,
RELATION,
AREA
}
/**
* Return a bounding box covering the element
* @return
*/
public abstract BoundingBox getBounds();
}