/**
* Created by Nicholas Hallahan on 1/2/15.
* nhallahan@spatialdev.com
*/
package com.spatialdev.osm.model;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.spatialdev.osm.OSMUtil;
import com.spatialdev.osm.renderer.OSMPath;
import com.vividsolutions.jts.geom.Geometry;
import org.xmlpull.v1.XmlSerializer;
public abstract class OSMElement {
private static LinkedList<OSMElement> selectedElements = new LinkedList<>();
private static boolean selectedElementsChanged = false;
private static LinkedList<OSMElement> modifiedElements = new LinkedList<>();
private static LinkedList<OSMElement> modifiedElementsInInstance = new LinkedList<>();
/**
* When creating a new OSMElement, it needs to be assigned a unique negative ID within
* the dataset. This should only be access by OSMElement#getUniqueNegativeId().
*/
private static long negativeId = -1;
protected long id;
protected long version;
protected String timestamp;
protected long changeset;
protected long uid;
protected String user;
protected boolean selected = false;
// set to true if the tags for this element have been modified
protected boolean modified = false;
// set to true if the application modifies tags for this element in this instance
protected boolean modifiedInInstance = false;
protected Geometry jtsGeom;
/**
* These tags get modified by the application
*/
protected Map<String, String> tags = new LinkedHashMap<>();
/**
* This can be used to keep track of which tag is currently selected in a tag editor
* like OpenMapKit.
* * *
*/
protected String selectedTag;
/**
* These tags are the original tags in the data set. This SHOULD NOT BE MODIFIED.
*/
protected Map<String, String> originalTags = new LinkedHashMap<>();
/**
* This is the object that actually gets drawn by OSMOverlay.
*/
protected OSMPath osmPath;
/**
* Elements that have been put in a select state*
* @return
*/
public static LinkedList<OSMElement> getSelectedElements() {
return selectedElements;
}
/**
* All of the modified elements we've got in memory, including those in previous
* edits of previous survey instances that have been scraped from ODK Collect.
* * * *
* @return all modified OSMElements
*/
public static LinkedList<OSMElement> getModifiedElements() {
return modifiedElements;
}
/**
* Only the modified elements that have had their tags modified in this survey instance
* * *
* @return elements with modified tags in this survey instance
*/
public static LinkedList<OSMElement> getModifiedElementsInInstance() {
return modifiedElementsInInstance;
}
public static boolean hasSelectedElementsChanged() {
if (selectedElementsChanged) {
selectedElementsChanged = false;
return true;
}
return false;
}
public static void deselectAll() {
for (OSMElement el : selectedElements) {
selectedElementsChanged = true;
el.deselect();
}
}
/**
* This constructor is used by OSMDataSet in the XML parsing process.
*/
public OSMElement(String idStr,
String versionStr,
String timestampStr,
String changesetStr,
String uidStr,
String userStr,
String action) {
try {
id = Long.valueOf(idStr);
} catch (Exception e) {
// dont assign
}
try {
version = Long.valueOf(versionStr);
} catch (Exception e) {
// dont assign
}
try {
timestamp = timestampStr;
} catch (Exception e) {
// dont assign
}
try {
changeset = Long.valueOf(changesetStr);
} catch (Exception e) {
// dont assign
}
try {
uid = Long.valueOf(uidStr);
} catch (Exception e) {
// dont assign
}
try {
user = userStr;
} catch (Exception e) {
// dont assign
}
if (action != null && action.equals("modify")) {
setAsModified();
}
}
/**
* This constructor is used when we are creating an new OSMElement,
* such as when a new Node is created. This constructor assumes
* that we are creating a NEW element in the current survey.
*/
public OSMElement() {
id = getUniqueNegativeId();
setAsModifiedInInstance();
}
protected static long getUniqueNegativeId() {
return negativeId--;
}
/**
* All OSM Element types need to have this implemented. This checksum is composed
* of the tags sorted alphabetically by key. The rest of the implementation is
* defined differently whether it is Node, Way, or Relation.
*
* @return SHA-1 HEX checksum of the element
*/
public abstract String checksum();
/**
* The tags are sorted by key, and each key, value is
* iterated and concatenated to a String.
*
* @return
*/
public StringBuilder tagsAsSortedKVString() {
List<String> keys = new ArrayList<>(tags.keySet());
java.util.Collections.sort(keys);
StringBuilder tagsStr = new StringBuilder();
for (String k : keys) {
String v = tags.get(k);
if (v.length() > 0) {
tagsStr.append(k);
tagsStr.append(v);
}
}
return tagsStr;
}
void xml(XmlSerializer xmlSerializer, String omkOsmUser) throws IOException {
// set the tags for the element (all element types can have tags)
Set<String> tagKeys = tags.keySet();
for (String tagKey : tagKeys) {
String tagVal = tags.get(tagKey);
if (tagVal == null || tagVal.equals("")) {
continue;
}
xmlSerializer.startTag(null, "tag");
xmlSerializer.attribute(null, "k", tagKey);
xmlSerializer.attribute(null, "v", tagVal);
xmlSerializer.endTag(null, "tag");
}
}
protected void setOsmElementXmlAttributes(XmlSerializer xmlSerializer, String omkOsmUser) throws IOException {
xmlSerializer.attribute(null, "id", String.valueOf(id));
if (isModified()) {
xmlSerializer.attribute(null, "action", "modify");
}
if (version != 0) {
xmlSerializer.attribute(null, "version", String.valueOf(version));
}
if (changeset != 0) {
xmlSerializer.attribute(null, "changeset", String.valueOf(changeset));
}
/**
* If the element just got modified, we want to set the time stamp when the record
* is serialized. If it has not been modified or was modified in a previous session,
* we want to stay with the previously recorded timestamp.
*/
if (modifiedInInstance) {
xmlSerializer.attribute(null, "timestamp", OSMUtil.nowTimestamp());
} else if (timestamp != null) {
xmlSerializer.attribute(null, "timestamp", timestamp);
}
/**
* We want to put the OSM user set in OMK Android for all of the elements we are writing.
* This is important, because when we are filtering in OMK iD, we need to be able to filter
* by OSM user. The OSM user should refer to all elements affected by an edit. This means
* that the OMK Android OSM user name should apply to nodes referenced by an edited way
* as well (so that filtering gets the complete geometry in).
*/
xmlSerializer.attribute(null, "user", omkOsmUser);
}
/**
* Maintains state over which tag is selected in a tag editor UI
* * *
* @param tagKey
*/
public void selectTag(String tagKey) {
selectedTag = tagKey;
}
/**
* If a tag is edited or added, this should be called by the application.*
* @param k
* @param v
*/
public void addOrEditTag(String k, String v) {
// OSM requires tag keys and values to not have trailing whitespaces.
String trimKey = k.trim();
String trimVal = v.trim();
String origVal = tags.get(trimKey);
// if the original tag is the same as this, we're not really editing anything.
if (trimVal.equals(origVal)) {
return;
}
setAsModifiedInInstance();
tags.put(trimKey, trimVal);
}
/**
* If the user removes a tag, call this method with the key of the tag.*
* @param k
*/
public void deleteTag(String k) {
String origVal = tags.get(k);
// Don't do anything if we are not deleting anything.
if (origVal == null) {
return;
}
setAsModifiedInInstance();
tags.remove(k);
}
public boolean isModified() {
return modified;
}
/**
* Any element that has been modified, either in the current instance or in previous
* survey instances.
* * * *
*/
protected void setAsModified() {
modified = true;
modifiedElements.add(this);
}
/**
* This is when an element is modified in this survey instance rather than a previous survey.
* We need to know this so that the edits can be written to OSM XML in ODK Collect.
* * *
*/
private void setAsModifiedInInstance() {
setAsModified();
modifiedInInstance = true;
modifiedElementsInInstance.add(this);
}
/**
* This should only be used by the parser.
* @param k
* @param v
*/
public void addParsedTag(String k, String v) {
originalTags.put(k, v);
tags.put(k, v);
}
public long getId() {
return id;
}
public Map<String, String> getTags() {
return tags;
}
public int getTagCount() {
return tags.size();
}
public void setJTSGeom(Geometry geom) {
jtsGeom = geom;
}
public Geometry getJTSGeom() {
return jtsGeom;
}
public void select() {
selectedElementsChanged = true;
selected = true;
selectedElements.push(this);
if (osmPath != null) {
osmPath.select();
}
}
public void deselect() {
selectedElementsChanged = true;
selected = false;
selectedElements.remove(this);
if (osmPath != null) {
osmPath.deselect();
}
}
public void toggle() {
if (selected) {
deselect();
} else {
select();
}
}
public boolean isSelected() {
return selected;
}
}