/* Copyright (c) 2013-2014 Boundless and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Distribution License v1.0 * which accompanies this distribution, and is available at * https://www.eclipse.org/org/documents/edl-v10.html * * Contributors: * Victor Olaya (Boundless) - initial implementation */ package org.locationtech.geogig.osm.internal; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import org.geotools.feature.simple.SimpleFeatureBuilder; import org.locationtech.geogig.api.AbstractGeoGigOp; import org.locationtech.geogig.api.NodeRef; import org.locationtech.geogig.api.ObjectId; import org.locationtech.geogig.api.RevFeature; import org.locationtech.geogig.api.RevFeatureType; import org.locationtech.geogig.api.RevFeatureTypeImpl; import org.locationtech.geogig.api.RevTree; import org.locationtech.geogig.api.plumbing.DiffTree; import org.locationtech.geogig.api.plumbing.LsTreeOp; import org.locationtech.geogig.api.plumbing.LsTreeOp.Strategy; import org.locationtech.geogig.api.plumbing.RevObjectParse; import org.locationtech.geogig.api.plumbing.diff.DiffEntry; import org.locationtech.geogig.osm.internal.MappingRule.DefaultField; import org.locationtech.geogig.osm.internal.log.OSMMappingLogEntry; import org.locationtech.geogig.osm.internal.log.ReadOSMMapping; import org.locationtech.geogig.osm.internal.log.ReadOSMMappingLogEntry; import org.opengis.feature.Property; import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.feature.type.PropertyDescriptor; import org.openstreetmap.osmosis.core.domain.v0_6.Tag; import com.beust.jcommander.internal.Maps; import com.google.common.base.Joiner; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryFactory; import com.vividsolutions.jts.geom.LineString; import com.vividsolutions.jts.geom.Point; /** * Updates the raw OSM data of the repository (stored in the "node" and "way" trees), with the data * in a tree that represents a mapped version of that raw data */ public class OSMUnmapOp extends AbstractGeoGigOp<RevTree> { /** * To avoid having to create a feature from the revFeature object when the unmapped feature * already exist, in order to access the field by name, we compute field indexes in advance * corresponding to the "way" and "node" types */ private static final RevFeatureType nodeType = RevFeatureTypeImpl.build(OSMUtils.nodeType()); private static final RevFeatureType wayType = RevFeatureTypeImpl.build(OSMUtils.wayType()); private static final int NODE_TAGS_FIELD_INDEX = getPropertyIndex(nodeType, "tags"); private static final int NODE_USER_FIELD_INDEX = getPropertyIndex(nodeType, "user"); private static final int NODE_TIMESTAMP_FIELD_INDEX = getPropertyIndex(nodeType, "timestamp"); private static final int NODE_VERSION_FIELD_INDEX = getPropertyIndex(nodeType, "version"); private static final int NODE_CHANGESET_FIELD_INDEX = getPropertyIndex(nodeType, "changeset"); private static final int NODE_LOCATION_FIELD_INDEX = getPropertyIndex(nodeType, "location"); private static final int WAY_TAGS_FIELD_INDEX = getPropertyIndex(wayType, "tags"); private static final int WAY_TIMESTAMP_FIELD_INDEX = getPropertyIndex(wayType, "timestamp"); private static final int WAY_VERSION_FIELD_INDEX = getPropertyIndex(wayType, "version"); private static final int WAY_CHANGESET_FIELD_INDEX = getPropertyIndex(wayType, "changeset"); private static final int WAY_USER_FIELD_INDEX = getPropertyIndex(wayType, "user"); final String UNKNOWN_USER = "Unknown"; private static int getPropertyIndex(RevFeatureType type, String name) { ImmutableList<PropertyDescriptor> descriptors = type.sortedDescriptors(); for (int i = 0; i < descriptors.size(); i++) { if (descriptors.get(i).getName().getLocalPart().equals(name)) { return i; } } // shouldn't reach this throw new RuntimeException("wrong field name"); } private String path; private Mapping mapping; private static GeometryFactory gf = new GeometryFactory(); /** * Sets the path to take the mapped data from * * @param path * @return */ public OSMUnmapOp setPath(String path) { this.path = path; return this; } @Override protected RevTree _call() { Optional<OSMMappingLogEntry> entry = command(ReadOSMMappingLogEntry.class).setPath(path) .call(); if (entry.isPresent()) { Optional<Mapping> opt = command(ReadOSMMapping.class).setEntry(entry.get()).call(); if (opt.isPresent()) { mapping = opt.get(); } } Iterator<NodeRef> iter = command(LsTreeOp.class).setReference(path) .setStrategy(Strategy.FEATURES_ONLY).call(); FeatureMapFlusher flusher = new FeatureMapFlusher(workingTree()); while (iter.hasNext()) { NodeRef node = iter.next(); RevFeature revFeature = command(RevObjectParse.class).setObjectId(node.objectId()) .call(RevFeature.class).get(); RevFeatureType revFeatureType = command(RevObjectParse.class) .setObjectId(node.getMetadataId()).call(RevFeatureType.class).get(); List<PropertyDescriptor> descriptors = revFeatureType.sortedDescriptors(); ImmutableList<Optional<Object>> values = revFeature.getValues(); SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder( (SimpleFeatureType) revFeatureType.type()); String id = null; for (int i = 0; i < descriptors.size(); i++) { PropertyDescriptor descriptor = descriptors.get(i); if (descriptor.getName().getLocalPart().equals("id")) { id = values.get(i).get().toString(); } Optional<Object> value = values.get(i); featureBuilder.set(descriptor.getName(), value.orNull()); } Preconditions.checkNotNull(id, "No 'id' attribute found"); SimpleFeature feature = featureBuilder.buildFeature(id); unmapFeature(feature, flusher); } flusher.flushAll(); // The above code will unmap all added or modified elements, but not deleted ones. // We now process the deletions, by comparing the current state of the mapped tree // with its state just after the mapping was created. if (entry.isPresent()) { Iterator<DiffEntry> diffs = command(DiffTree.class).setPathFilter(path) .setNewTree(workingTree().getTree().getId()) .setOldTree(entry.get().getPostMappingId()).call(); while (diffs.hasNext()) { DiffEntry diff = diffs.next(); if (diff.changeType().equals(DiffEntry.ChangeType.REMOVED)) { ObjectId featureId = diff.getOldObject().getNode().getObjectId(); RevFeature revFeature = command(RevObjectParse.class).setObjectId(featureId) .call(RevFeature.class).get(); RevFeatureType revFeatureType = command(RevObjectParse.class) .setObjectId(diff.getOldObject().getMetadataId()) .call(RevFeatureType.class).get(); List<PropertyDescriptor> descriptors = revFeatureType.sortedDescriptors(); ImmutableList<Optional<Object>> values = revFeature.getValues(); SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder( (SimpleFeatureType) revFeatureType.type()); String id = null; for (int i = 0; i < descriptors.size(); i++) { PropertyDescriptor descriptor = descriptors.get(i); if (descriptor.getName().getLocalPart().equals("id")) { id = values.get(i).get().toString(); } Optional<Object> value = values.get(i); featureBuilder.set(descriptor.getName(), value.orNull()); } Preconditions.checkNotNull(id, "No 'id' attribute found"); SimpleFeature feature = featureBuilder.buildFeature(id); Class<?> clazz = feature.getDefaultGeometryProperty().getType().getBinding(); String deletePath = clazz.equals(Point.class) ? OSMUtils.NODE_TYPE_NAME : OSMUtils.WAY_TYPE_NAME; workingTree().delete(deletePath, id); } } } return workingTree().getTree(); } private void unmapFeature(SimpleFeature feature, FeatureMapFlusher mapFlusher) { Class<?> clazz = feature.getDefaultGeometryProperty().getType().getBinding(); if (clazz.equals(Point.class)) { unmapNode(feature, mapFlusher); } else { unmapWay(feature, mapFlusher); } } private void unmapNode(SimpleFeature feature, FeatureMapFlusher mapFlusher) { boolean modified = false; String id = feature.getID(); SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder(OSMUtils.nodeType()); Optional<RevFeature> rawFeature = command(RevObjectParse.class).setRefSpec( "WORK_HEAD:" + OSMUtils.NODE_TYPE_NAME + "/" + id).call(RevFeature.class); Map<String, String> tagsMap = Maps.newHashMap(); long timestamp = System.currentTimeMillis(); int version = 1; long changeset = -1; String user = UNKNOWN_USER; Collection<Tag> tags = Lists.newArrayList(); if (rawFeature.isPresent()) { ImmutableList<Optional<Object>> values = rawFeature.get().getValues(); tags = OSMUtils.buildTagsCollectionFromString(values.get(NODE_TAGS_FIELD_INDEX).get() .toString()); for (Tag tag : tags) { tagsMap.put(tag.getKey(), tag.getValue()); } Optional<Object> timestampOpt = values.get(NODE_TIMESTAMP_FIELD_INDEX); if (timestampOpt.isPresent()) { timestamp = ((Long) timestampOpt.get()).longValue(); } Optional<Object> versionOpt = values.get(NODE_VERSION_FIELD_INDEX); if (versionOpt.isPresent()) { version = ((Integer) versionOpt.get()).intValue(); } Optional<Object> changesetOpt = values.get(NODE_CHANGESET_FIELD_INDEX); if (changesetOpt.isPresent()) { changeset = ((Long) changesetOpt.get()).longValue(); } Optional<Object> userOpt = values.get(NODE_USER_FIELD_INDEX); if (userOpt.isPresent()) { user = (String) userOpt.get(); } } Map<String, String> unaliased = Maps.newHashMap(); Collection<Property> properties = feature.getProperties(); for (Property property : properties) { String name = property.getName().getLocalPart(); if (name.equals("id") || Geometry.class.isAssignableFrom(property.getDescriptor().getType() .getBinding())) { continue; } Object value = property.getValue(); if (value != null) { String tagName = name; if (mapping != null) { if (unaliased.containsKey(name)) { tagName = unaliased.get(name); } else { tagName = mapping.getTagNameFromAlias(path, tagName); unaliased.put(name, tagName); } } if (!DefaultField.isDefaultField(tagName)) { if (tagsMap.containsKey(tagName)) { if (!modified) { String oldValue = tagsMap.get(tagName); modified = !value.equals(oldValue); } } else { modified = true; } tagsMap.put(tagName, value.toString()); } } } if (!modified && rawFeature.isPresent()) { // no changes after unmapping tags, so there's nothing else to do return; } Collection<Tag> newTags = Lists.newArrayList(); Set<Entry<String, String>> entries = tagsMap.entrySet(); for (Entry<String, String> entry : entries) { newTags.add(new Tag(entry.getKey(), entry.getValue())); } featureBuilder.set("tags", OSMUtils.buildTagsString(newTags)); featureBuilder.set("location", feature.getDefaultGeometry()); featureBuilder.set("changeset", changeset); featureBuilder.set("timestamp", timestamp); featureBuilder.set("version", version); featureBuilder.set("user", user); featureBuilder.set("visible", true); if (rawFeature.isPresent()) { // the feature has changed, so we cannot reuse some attributes. // We reconstruct the feature and insert it featureBuilder.set("timestamp", System.currentTimeMillis()); featureBuilder.set("changeset", null); featureBuilder.set("version", null); featureBuilder.set("visible", true); mapFlusher.put("node", featureBuilder.buildFeature(id)); } else { // The feature didn't exist, so we have to add it mapFlusher.put("node", featureBuilder.buildFeature(id)); } } /** * This method takes a way and generates the corresponding string with ids of nodes than compose * that line. If those nodes are declared in the "nodes" attribute of the feature, they will be * referenced and no new node will be added. If a coordinate in the linestring doesn't have a * corresponding node declared in the "nodes" attribute, a new node at that coordinate will be * added to the "node" tree. This way, the returned linestring is guaranteed to refer to nodes * that already exist in the repository, and as such can be safely used to add a new way that * uses those nodes. * * @param line * @return */ private String getNodeStringFromWay(SimpleFeature way, FeatureMapFlusher flusher) { Map<Coordinate, Long> nodeCoords = Maps.newHashMap(); Object nodesAttribute = way.getAttribute("nodes"); if (nodesAttribute != null) { String[] nodeIds = nodesAttribute.toString().split(";"); for (String nodeId : nodeIds) { Optional<RevFeature> revFeature = command(RevObjectParse.class).setRefSpec( "WORK_HEAD:" + OSMUtils.NODE_TYPE_NAME + "/" + nodeId).call( RevFeature.class); if (revFeature.isPresent()) { Optional<Object> location = revFeature.get().getValues() .get(NODE_LOCATION_FIELD_INDEX); if (location.isPresent()) { Coordinate coord = ((Geometry) location.get()).getCoordinate(); nodeCoords.put(coord, Long.parseLong(nodeId)); } } } } List<Long> nodes = Lists.newArrayList(); Coordinate[] coords = ((Geometry) way.getDefaultGeometryProperty().getValue()) .getCoordinates(); for (Coordinate coord : coords) { if (nodeCoords.containsKey(coord)) { nodes.add(nodeCoords.get(coord)); } else { nodes.add(createNodeForCoord(coord, flusher)); } } return Joiner.on(';').join(nodes); } /** * Creates a new node at a given a given coordinate and inserts it into the working tree * * @param coord * @return the id of the created node */ private Long createNodeForCoord(Coordinate coord, FeatureMapFlusher flusher) { long id = -1 * System.currentTimeMillis(); // TODO: This has to be changed!!! SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder(OSMUtils.nodeType()); featureBuilder.set("tags", null); featureBuilder.set("location", gf.createPoint(coord)); featureBuilder.set("changeset", null); featureBuilder.set("timestamp", System.currentTimeMillis()); featureBuilder.set("version", 1); featureBuilder.set("user", null); featureBuilder.set("visible", true); flusher.put("node", featureBuilder.buildFeature(Long.toString(id))); return id; } private void unmapWay(SimpleFeature feature, FeatureMapFlusher flusher) { boolean modified = false; String id = feature.getID(); SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder(OSMUtils.wayType()); Optional<RevFeature> rawFeature = command(RevObjectParse.class).setRefSpec( "WORK_HEAD:" + OSMUtils.WAY_TYPE_NAME + "/" + id).call(RevFeature.class); Map<String, String> tagsMap = Maps.newHashMap(); long timestamp = System.currentTimeMillis(); int version = 1; long changeset = -1; String user = UNKNOWN_USER; Collection<Tag> tags = Lists.newArrayList(); if (rawFeature.isPresent()) { ImmutableList<Optional<Object>> values = rawFeature.get().getValues(); tags = OSMUtils.buildTagsCollectionFromString(values.get(WAY_TAGS_FIELD_INDEX).get() .toString()); for (Tag tag : tags) { tagsMap.put(tag.getKey(), tag.getValue()); } Optional<Object> timestampOpt = values.get(WAY_TIMESTAMP_FIELD_INDEX); if (timestampOpt.isPresent()) { timestamp = ((Long) timestampOpt.get()).longValue(); } Optional<Object> versionOpt = values.get(WAY_VERSION_FIELD_INDEX); if (versionOpt.isPresent()) { version = ((Integer) versionOpt.get()).intValue(); } Optional<Object> changesetOpt = values.get(WAY_CHANGESET_FIELD_INDEX); if (changesetOpt.isPresent()) { changeset = ((Long) changesetOpt.get()).longValue(); } Optional<Object> userOpt = values.get(WAY_USER_FIELD_INDEX); if (userOpt.isPresent()) { user = (String) userOpt.get(); } } Map<String, String> unaliased = Maps.newHashMap(); Collection<Property> properties = feature.getProperties(); for (Property property : properties) { String name = property.getName().getLocalPart(); if (name.equals("id") || name.equals("nodes") || Geometry.class.isAssignableFrom(property.getDescriptor().getType() .getBinding())) { continue; } Object value = property.getValue(); if (value != null) { String tagName = name; if (mapping != null) { if (unaliased.containsKey(name)) { tagName = unaliased.get(name); } else { tagName = mapping.getTagNameFromAlias(path, tagName); unaliased.put(name, tagName); } } if (!DefaultField.isDefaultField(tagName)) { if (tagsMap.containsKey(tagName)) { if (!modified) { String oldValue = tagsMap.get(tagName); modified = !value.equals(oldValue); } } else { modified = true; } tagsMap.put(tagName, value.toString()); } } } if (!modified && rawFeature.isPresent()) { // no changes after unmapping tags, so there's nothing else to do return; } tags.clear(); Set<Entry<String, String>> entries = tagsMap.entrySet(); for (Entry<String, String> entry : entries) { tags.add(new Tag(entry.getKey(), entry.getValue())); } Geometry geom = (Geometry) feature.getDefaultGeometry(); LineString line; if (geom instanceof LineString) { line = (LineString) geom; } else { line = gf.createLineString(geom.getCoordinates()); } featureBuilder.set("visible", true); featureBuilder.set("tags", OSMUtils.buildTagsString(tags)); featureBuilder.set("way", line); featureBuilder.set("changeset", changeset); featureBuilder.set("timestamp", timestamp); featureBuilder.set("version", version); featureBuilder.set("user", user); featureBuilder.set("nodes", getNodeStringFromWay(feature, flusher)); if (rawFeature.isPresent()) { // the feature has changed, so we cannot reuse some attributes featureBuilder.set("timestamp", System.currentTimeMillis()); featureBuilder.set("changeset", -changeset); // temporary negative changeset ID // featureBuilder.set("version", version); flusher.put("way", featureBuilder.buildFeature(id)); } else { flusher.put("way", featureBuilder.buildFeature(id)); } } }