Ticket #21788: 21788.patch
| File 21788.patch, 20.9 KB (added by , 4 years ago) |
|---|
-
src/com/vividsolutions/jcs/conflate/polygonmatch/AngleHistogramMatcher.java
84 84 * weighted by segment length 85 85 */ 86 86 protected Histogram angleHistogram(Geometry g, int binCount) { 87 Geometry clone = (Geometry) g.clone();87 Geometry clone = g.copy(); 88 88 //#normalize makes linestrings and polygons use a standard orientation. 89 89 //[Jon Aquino] 90 90 clone.normalize(); -
src/com/vividsolutions/jcs/conflate/polygonmatch/CentroidAligner.java
16 16 } 17 17 18 18 private Geometry align(Geometry original) { 19 Geometry aligned = (Geometry) original.clone();19 Geometry aligned = original.copy(); 20 20 MatcherUtil.align(aligned, aligned.getCentroid().getCoordinate()); 21 21 return aligned; 22 22 } -
src/com/vividsolutions/jcs/conflate/polygonmatch/Matches.java
31 31 */ 32 32 package com.vividsolutions.jcs.conflate.polygonmatch; 33 33 34 import java.util.AbstractMap; 34 35 import java.util.ArrayList; 35 36 import java.util.Collection; 37 import java.util.HashSet; 36 38 import java.util.Iterator; 37 39 import java.util.List; 40 import java.util.Map; 41 import java.util.Set; 38 42 39 43 import org.locationtech.jts.geom.Envelope; 40 44 import org.locationtech.jts.util.Assert; … … 47 51 * A FeatureCollection that stores the "score" of each Feature. The score is 48 52 * a number between 0.0 and 1.0 that indicates the confidence of a match. 49 53 */ 50 public class Matches implements FeatureCollection, Cloneable {51 54 public class Matches extends AbstractMap<Feature, Double> implements FeatureCollection, Cloneable { 55 private final Set<Map.Entry<Feature, Double>> entrySet = new HashSet<>(); 52 56 /** 53 57 * Creates a Matches object. 54 58 * @param schema metadata applicable to the features that will be stored in … … 80 84 } 81 85 } 82 86 83 private FeatureDataset dataset;84 private List<Double> scores = new ArrayList<>();87 private final FeatureDataset dataset; 88 private final List<Double> scores = new ArrayList<>(); 85 89 86 90 /** 87 91 * This method is not supported, because added features need to be associated … … 135 139 throw new UnsupportedOperationException(); 136 140 } 137 141 142 @Override 143 public Set<Entry<Feature, Double>> entrySet() { 144 if (this.size() == this.entrySet.size()) { 145 return this.entrySet; 146 } 147 return updateEntrySet(); 148 } 149 150 private synchronized Set<Entry<Feature, Double>> updateEntrySet() { 151 if (this.size() != this.entrySet.size()) { 152 this.entrySet.clear(); 153 for (int i = 0; i < this.size(); i++) { 154 this.entrySet.add(new SimpleEntry<>(this.getFeature(i), this.getScore(i))); 155 } 156 } 157 return this.entrySet; 158 } 159 138 160 /** 139 161 * This method is not supported, because Matches should not normally need to 140 162 * have matches removed. … … 167 189 if (score == 0) { 168 190 return; 169 191 } 170 scores.add( Double.valueOf(score));192 scores.add(score); 171 193 dataset.add(feature); 172 194 if (score > topScore) { 173 195 topScore = score; … … 195 217 * @return the confidence of the ith match 196 218 */ 197 219 public double getScore(int i) { 198 return scores.get(i) .doubleValue();220 return scores.get(i); 199 221 } 200 222 201 223 @Override -
src/com/vividsolutions/jcs/conflate/polygonmatch/SymDiffMatcher.java
53 53 */ 54 54 @Override 55 55 public double match(Geometry target, Geometry candidate) { 56 Geometry targetGeom = (Geometry) target.clone();57 Geometry candidateGeom = (Geometry) candidate.clone();56 Geometry targetGeom = target.copy(); 57 Geometry candidateGeom = candidate.copy(); 58 58 if (targetGeom.isEmpty() || candidateGeom.isEmpty()) { 59 59 return 0; //avoid div by 0 in centre-of-mass calc [Jon Aquino] 60 60 } -
src/com/vividsolutions/jcs/conflate/polygonmatch/WeightedMatcher.java
33 33 34 34 import java.util.HashMap; 35 35 import java.util.Map; 36 import java.util.Objects; 36 37 import java.util.TreeMap; 37 38 38 39 import org.locationtech.jts.util.Assert; … … 48 49 49 50 /** 50 51 * Creates a WeightedMatcher with the given matchers and their weights. 51 * @param matchersAndWeights alternates between FeatureMatchers and Doubles52 * @param matchersAndWeights alternates between Doubles and FeatureMatchers 52 53 */ 53 public WeightedMatcher(Object[] matchersAndWeights) { 54 public WeightedMatcher(Object... matchersAndWeights) { 55 Objects.requireNonNull(matchersAndWeights); 54 56 Assert.isTrue(matchersAndWeights.length % 2 == 0); 55 57 for (int i = 0; i < matchersAndWeights.length; i += 2) { 56 58 add((FeatureMatcher) matchersAndWeights[i+1], … … 70 72 if (weight == 0) { 71 73 return; 72 74 } 73 matcherToWeightMap.put(matcher, Double.valueOf(weight));75 this.matcherToWeightMap.put(matcher, weight); 74 76 } 75 77 76 private Map<FeatureMatcher, Double> matcherToWeightMap = new HashMap<>();78 private final Map<FeatureMatcher, Double> matcherToWeightMap = new HashMap<>(); 77 79 78 80 /** 79 81 * Searches a collection of candidate features for those that match the given … … 93 95 94 96 private Matches toMatches(Map<Feature, Double> featureToScoreMap, FeatureSchema schema) { 95 97 Matches matches = new Matches(schema); 96 for (Feature feature : featureToScoreMap.keySet()) { 97 double score = featureToScoreMap.get(feature).doubleValue(); 98 matches.add(feature, score); 99 } 98 featureToScoreMap.forEach(matches::add); 100 99 return matches; 101 100 } 102 101 … … 111 110 112 111 private Map<Feature, Double> featureToScoreMap(Map<FeatureMatcher, Matches> matcherToMatchesMap) { 113 112 Map<Feature, Double> featureToScoreMap = new TreeMap<>(); 114 for (FeatureMatcher matcher : matcherToMatchesMap.keySet()) { 115 Matches matches = matcherToMatchesMap.get(matcher); 116 addToFeatureToScoreMap(matches, matcher, featureToScoreMap); 117 } 113 matcherToMatchesMap.forEach((matcher, matches) -> this.addToFeatureToScoreMap(matches, matcher, featureToScoreMap)); 118 114 return featureToScoreMap; 119 115 } 120 116 121 117 private void addToFeatureToScoreMap(Matches matches, FeatureMatcher matcher, 122 118 Map<Feature, Double> featureToScoreMap) { 123 for (int i = 0; i < matches.size(); i++) { 124 double score = matches.getScore(i) * normalizedWeight(matcher); 125 addToFeatureToScoreMap(matches.getFeature(i), score, featureToScoreMap); 119 120 for (Map.Entry<Feature, Double> entry : matches.entrySet()) { 121 double score = entry.getValue() * this.normalizedWeight(matcher); 122 featureToScoreMap.compute(entry.getKey(), (key, value) -> value == null ? score : value + score); 126 123 } 127 124 } 128 125 129 private void addToFeatureToScoreMap(Feature feature, double score, Map<Feature, Double> featureToScoreMap) {130 Double oldScore = featureToScoreMap.get(feature);131 if (oldScore == null) { oldScore = Double.valueOf(0); }132 featureToScoreMap.put(feature, Double.valueOf(oldScore.doubleValue() + score));133 }134 135 126 private double normalizedWeight(FeatureMatcher matcher) { 136 return matcherToWeightMap.get(matcher).doubleValue() /weightTotal();127 return this.matcherToWeightMap.get(matcher) / this.weightTotal(); 137 128 } 138 129 139 130 private double weightTotal() { 140 double weightTotal = 0; 141 for (Double weight : matcherToWeightMap.values()) { 142 weightTotal += weight.doubleValue(); 143 } 144 return weightTotal; 131 return this.matcherToWeightMap.values().stream().mapToDouble(Double::doubleValue).sum(); 145 132 } 146 133 } -
src/com/vividsolutions/jump/feature/AbstractBasicFeature.java
182 182 for (int i = 0; i < schema.getAttributeCount(); i++) { 183 183 if (schema.getAttributeType(i) == AttributeType.GEOMETRY) { 184 184 clone.setAttribute(i, 185 deep ? getGeometry().c lone() : getGeometry());185 deep ? getGeometry().copy() : getGeometry()); 186 186 } else { 187 187 clone.setAttribute(i, getAttribute(i)); 188 188 } -
src/com/vividsolutions/jump/util/CoordinateArrays.java
34 34 import java.util.ArrayList; 35 35 import java.util.List; 36 36 37 import org.locationtech.jts.algorithm. CGAlgorithms;37 import org.locationtech.jts.algorithm.Orientation; 38 38 import org.locationtech.jts.geom.Coordinate; 39 39 import org.locationtech.jts.geom.Geometry; 40 40 import org.locationtech.jts.geom.GeometryCollection; … … 128 128 Coordinate[] shell = poly.getExteriorRing().getCoordinates(); 129 129 130 130 if (orientPolygons) { 131 shell = ensureOrientation(shell, CGAlgorithms.CLOCKWISE);131 shell = ensureOrientation(shell, Orientation.CLOCKWISE); 132 132 } 133 133 134 134 coordArrayList.add(shell); … … 137 137 Coordinate[] hole = poly.getInteriorRingN(i).getCoordinates(); 138 138 139 139 if (orientPolygons) { 140 hole = ensureOrientation(hole, CGAlgorithms.COUNTERCLOCKWISE);140 hole = ensureOrientation(hole, Orientation.COUNTERCLOCKWISE); 141 141 } 142 142 143 143 coordArrayList.add(hole); … … 168 168 return coord; 169 169 } 170 170 171 int orientation = CGAlgorithms.isCCW(coord) ? CGAlgorithms.COUNTERCLOCKWISE172 : CGAlgorithms.CLOCKWISE;171 int orientation = Orientation.isCCW(coord) ? Orientation.COUNTERCLOCKWISE 172 : Orientation.CLOCKWISE; 173 173 174 174 if (orientation != desiredOrientation) { 175 175 Coordinate[] reverse = coord.clone(); -
src/org/openstreetmap/josm/plugins/conflation/OsmFeature.java
3 3 package org.openstreetmap.josm.plugins.conflation; 4 4 5 5 import java.util.Map; 6 import java.util.Objects; 6 7 7 8 import org.openstreetmap.josm.data.osm.OsmPrimitive; 8 9 import org.openstreetmap.josm.plugins.jts.JTSConverter; … … 9 10 10 11 import com.vividsolutions.jump.feature.AbstractBasicFeature; 11 12 import com.vividsolutions.jump.feature.AttributeType; 13 import com.vividsolutions.jump.feature.Feature; 12 14 import com.vividsolutions.jump.feature.FeatureSchema; 13 15 14 16 public class OsmFeature extends AbstractBasicFeature { 15 17 private Object[] attributes; 16 private OsmPrimitive primitive; 17 private JTSConverter converter; 18 private final OsmPrimitive primitive; 18 19 19 20 /** 20 21 * Create a copy of the OSM geometry … … 22 23 */ 23 24 public OsmFeature(OsmPrimitive prim, JTSConverter jtsConverter) { 24 25 super(new FeatureSchema()); 25 primitive = prim;26 this.primitive = Objects.requireNonNull(prim); 26 27 Map<String, String> keys = prim.getKeys(); 27 attributes = new Object[keys.size() + 1]; 28 getSchema().addAttribute("__GEOMETRY__", AttributeType.GEOMETRY); 29 for (String key : keys.keySet()) { 30 getSchema().addAttribute(key, AttributeType.STRING); 31 setAttribute(key, keys.get(key)); 32 } 28 this.attributes = new Object[keys.size() + 1]; 29 this.getSchema().addAttribute("__GEOMETRY__", AttributeType.GEOMETRY); 30 keys.forEach((key, value) -> { 31 this.getSchema().addAttribute(key, AttributeType.STRING); 32 setAttribute(key, value); 33 }); 34 final JTSConverter converter; 33 35 if (jtsConverter != null) 34 36 converter = jtsConverter; 35 37 else … … 68 70 // objects with the same id 69 71 return (int) primitive.getUniqueId(); 70 72 } 73 74 @Override 75 public int compareTo(Feature abstractBasicFeature) { 76 // Rather unfortunately, we cannot implement the interface with OsmFeature 77 // So we are going to special case osm features, and compare ids. 78 // The super.compareTo only looks at geometry. If the geometry is the same, then it returns 0. 79 final int superCompare = super.compareTo(abstractBasicFeature); 80 if (superCompare == 0 && abstractBasicFeature instanceof OsmFeature) { 81 final OsmFeature other = (OsmFeature) abstractBasicFeature; 82 return this.primitive.compareTo(other.primitive); 83 } 84 return superCompare; 85 } 86 87 @Override 88 public boolean equals(Object other) { 89 return other != null && this.getClass().equals(other.getClass()) 90 && Objects.equals(((OsmFeature) other).primitive, this.primitive); 91 } 92 93 @Override 94 public int hashCode() { 95 // No superclasses implement hashCode 96 return this.primitive.hashCode(); 97 } 71 98 } -
test/unit/com/vividsolutions/jcs/conflate/polygonmatch/WeightedMatcherTest.java
1 package com.vividsolutions.jcs.conflate.polygonmatch; 2 3 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 4 5 import java.util.Arrays; 6 7 import org.junit.jupiter.api.Test; 8 import org.junit.jupiter.api.extension.RegisterExtension; 9 import org.openstreetmap.josm.data.coor.LatLon; 10 import org.openstreetmap.josm.data.osm.Node; 11 import org.openstreetmap.josm.plugins.conflation.OsmFeature; 12 import org.openstreetmap.josm.plugins.jts.JTSConverter; 13 import org.openstreetmap.josm.testutils.JOSMTestRules; 14 import org.openstreetmap.josm.testutils.annotations.BasicPreferences; 15 16 import com.vividsolutions.jump.feature.Feature; 17 import com.vividsolutions.jump.feature.FeatureCollection; 18 import com.vividsolutions.jump.feature.FeatureDataset; 19 import com.vividsolutions.jump.feature.FeatureSchema; 20 import com.vividsolutions.jump.feature.IndexedFeatureCollection; 21 22 /** 23 * Test class for {@link WeightedMatcher} 24 * @author Taylor Smock 25 */ 26 @BasicPreferences 27 class WeightedMatcherTest { 28 @RegisterExtension 29 JOSMTestRules josmTestRules = new JOSMTestRules().projection(); 30 /** 31 * Non-regression test for JOSM #21788 32 * This occurred when two {@link org.openstreetmap.josm.plugins.conflation.OsmFeature} objects had {@link Comparable} 33 * equality. 34 */ 35 @Test 36 void testNonRegression21788() { 37 final Node target = new Node(); 38 target.setCoor(LatLon.ZERO); 39 final Node node1 = new Node(1, 1); 40 node1.setCoor(LatLon.ZERO); 41 final Node node2 = new Node(2, 1); 42 node2.setCoor(LatLon.ZERO); 43 final JTSConverter converter = new JTSConverter(true); 44 final WeightedMatcher weightedMatcher = new WeightedMatcher(1, new UnityMatcher()); 45 assertDoesNotThrow(() -> weightedMatcher.match(new OsmFeature(target, converter), 46 new FeatureDataset(Arrays.asList(new OsmFeature(node1, converter), new OsmFeature(node2, converter)), new FeatureSchema()))); 47 } 48 49 /** 50 * This matcher always gives a score of 1. This is to make it easier for getting out of bounds. 51 */ 52 private static final class UnityMatcher implements FeatureMatcher { 53 @Override 54 public Matches match(Feature target, FeatureCollection candidates) { 55 final Matches matches = new Matches(target.getSchema()); 56 candidates.forEach(feature -> matches.add(feature, 1)); 57 return matches; 58 } 59 } 60 } -
test/unit/org/openstreetmap/josm/plugins/conflation/OsmFeatureTest.java
1 package org.openstreetmap.josm.plugins.conflation; 2 3 import static org.junit.jupiter.api.Assertions.assertEquals; 4 import static org.junit.jupiter.api.Assertions.assertNotEquals; 5 import static org.junit.jupiter.api.Assertions.assertTrue; 6 7 import java.util.Arrays; 8 9 import org.junit.jupiter.api.Test; 10 import org.junit.jupiter.api.extension.RegisterExtension; 11 import org.openstreetmap.josm.data.coor.LatLon; 12 import org.openstreetmap.josm.data.osm.Node; 13 import org.openstreetmap.josm.data.osm.OsmPrimitive; 14 import org.openstreetmap.josm.plugins.jts.JTSConverter; 15 import org.openstreetmap.josm.testutils.JOSMTestRules; 16 import org.openstreetmap.josm.testutils.annotations.BasicPreferences; 17 18 import com.vividsolutions.jump.feature.AttributeType; 19 import com.vividsolutions.jump.feature.FeatureSchema; 20 import nl.jqno.equalsverifier.EqualsVerifier; 21 22 /** 23 * Test class for {@link OsmFeature} 24 * @author Taylor Smock 25 */ 26 @BasicPreferences 27 class OsmFeatureTest { 28 @RegisterExtension 29 JOSMTestRules josmTestRules = new JOSMTestRules().projection(); 30 /** 31 * This checks that two osm features (points) do not match if they have the same geometry but different ids. 32 * This is a partial non-regression test for JOSM #21788. The root of the issue is that TreeMap uses the Comparable 33 * interface in put operations, and the {@link Comparable} interface specifies that {@code 0} implies equality. 34 */ 35 @Test 36 void testCompareTo() { 37 final JTSConverter converter = new JTSConverter(true); 38 39 final Node node1 = new Node(1, 1); 40 final Node node2 = new Node(2, 1); 41 // This is deliberate -- we want to make certain that different equal objects work (i.e., no one accidentally 42 // uses == ). 43 final Node node3 = new Node(1, 1); 44 Arrays.asList(node1, node2, node3).forEach(node -> node.setCoor(LatLon.ZERO)); 45 46 final OsmFeature osmFeature1 = new OsmFeature(node1, converter); 47 final OsmFeature osmFeature2 = new OsmFeature(node2, converter); 48 final OsmFeature osmFeature3 = new OsmFeature(node3, converter); 49 50 assertEquals(osmFeature1, osmFeature3, "The two nodes are equal"); 51 assertNotEquals(osmFeature1, osmFeature2, "The two nodes are not equal"); 52 assertEquals(0, osmFeature1.compareTo(osmFeature3)); 53 assertEquals(0, osmFeature3.compareTo(osmFeature1)); 54 assertNotEquals(0, osmFeature1.compareTo(osmFeature2)); 55 assertNotEquals(0, osmFeature2.compareTo(osmFeature3)); 56 } 57 58 @Test 59 void testEqualsContract() { 60 final Node redNode = new Node(1, 1); 61 final Node blueNode = new Node(2, 2); 62 redNode.setCoor(LatLon.NORTH_POLE); 63 blueNode.setCoor(LatLon.SOUTH_POLE); 64 65 final FeatureSchema redSchema = new FeatureSchema(); 66 final FeatureSchema blueSchema = new FeatureSchema(); 67 redSchema.addAttribute("red", AttributeType.STRING); 68 blueSchema.addAttribute("blue", AttributeType.STRING); 69 EqualsVerifier.forClass(OsmFeature.class) 70 .usingGetClass() 71 .withNonnullFields("primitive") 72 .withIgnoredFields("attributes" /* mutable */, 73 "schema" /* mutable */, 74 "id" /* not used in class */) 75 .withPrefabValues(OsmPrimitive.class, redNode, blueNode) 76 .withPrefabValues(FeatureSchema.class, redSchema, blueSchema) 77 .verify(); 78 } 79 }
