Ticket #21788: 21788.patch

File 21788.patch, 20.9 KB (added by taylor.smock, 4 years ago)
  • src/com/vividsolutions/jcs/conflate/polygonmatch/AngleHistogramMatcher.java

     
    8484     * weighted by segment length
    8585     */
    8686    protected Histogram angleHistogram(Geometry g, int binCount) {
    87         Geometry clone = (Geometry) g.clone();
     87        Geometry clone = g.copy();
    8888        //#normalize makes linestrings and polygons use a standard orientation.
    8989        //[Jon Aquino]
    9090        clone.normalize();
  • src/com/vividsolutions/jcs/conflate/polygonmatch/CentroidAligner.java

     
    1616    }
    1717
    1818    private Geometry align(Geometry original) {
    19         Geometry aligned = (Geometry) original.clone();
     19        Geometry aligned = original.copy();
    2020        MatcherUtil.align(aligned, aligned.getCentroid().getCoordinate());
    2121        return aligned;
    2222    }
  • src/com/vividsolutions/jcs/conflate/polygonmatch/Matches.java

     
    3131 */
    3232package com.vividsolutions.jcs.conflate.polygonmatch;
    3333
     34import java.util.AbstractMap;
    3435import java.util.ArrayList;
    3536import java.util.Collection;
     37import java.util.HashSet;
    3638import java.util.Iterator;
    3739import java.util.List;
     40import java.util.Map;
     41import java.util.Set;
    3842
    3943import org.locationtech.jts.geom.Envelope;
    4044import org.locationtech.jts.util.Assert;
     
    4751 * A FeatureCollection that stores the "score" of each Feature.  The score is
    4852 * a number between 0.0 and 1.0 that indicates the confidence of a match.
    4953 */
    50 public class Matches implements FeatureCollection, Cloneable {
    51 
     54public class Matches extends AbstractMap<Feature, Double> implements FeatureCollection, Cloneable {
     55    private final Set<Map.Entry<Feature, Double>> entrySet = new HashSet<>();
    5256    /**
    5357     * Creates a Matches object.
    5458     * @param schema metadata applicable to the features that will be stored in
     
    8084        }
    8185    }
    8286
    83     private FeatureDataset dataset;
    84     private List<Double> scores = new ArrayList<>();
     87    private final FeatureDataset dataset;
     88    private final List<Double> scores = new ArrayList<>();
    8589
    8690    /**
    8791     * This method is not supported, because added features need to be associated
     
    135139        throw new UnsupportedOperationException();
    136140    }
    137141
     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
    138160    /**
    139161     * This method is not supported, because Matches should not normally need to
    140162     * have matches removed.
     
    167189        if (score == 0) {
    168190            return;
    169191        }
    170         scores.add(Double.valueOf(score));
     192        scores.add(score);
    171193        dataset.add(feature);
    172194        if (score > topScore) {
    173195            topScore = score;
     
    195217     * @return the confidence of the ith match
    196218     */
    197219    public double getScore(int i) {
    198         return scores.get(i).doubleValue();
     220        return scores.get(i);
    199221    }
    200222
    201223    @Override
  • src/com/vividsolutions/jcs/conflate/polygonmatch/SymDiffMatcher.java

     
    5353   */
    5454  @Override
    5555  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();
    5858    if (targetGeom.isEmpty() || candidateGeom.isEmpty()) {
    5959      return 0; //avoid div by 0 in centre-of-mass calc [Jon Aquino]
    6060    }
  • src/com/vividsolutions/jcs/conflate/polygonmatch/WeightedMatcher.java

     
    3333
    3434import java.util.HashMap;
    3535import java.util.Map;
     36import java.util.Objects;
    3637import java.util.TreeMap;
    3738
    3839import org.locationtech.jts.util.Assert;
     
    4849
    4950    /**
    5051   * Creates a WeightedMatcher with the given matchers and their weights.
    51    * @param matchersAndWeights alternates between FeatureMatchers and Doubles
     52   * @param matchersAndWeights alternates between Doubles and FeatureMatchers
    5253   */
    53   public WeightedMatcher(Object[] matchersAndWeights) {
     54  public WeightedMatcher(Object... matchersAndWeights) {
     55    Objects.requireNonNull(matchersAndWeights);
    5456    Assert.isTrue(matchersAndWeights.length % 2 == 0);
    5557    for (int i = 0; i < matchersAndWeights.length; i += 2) {
    5658      add((FeatureMatcher) matchersAndWeights[i+1],
     
    7072    if (weight == 0) {
    7173        return;
    7274    }
    73     matcherToWeightMap.put(matcher, Double.valueOf(weight));
     75    this.matcherToWeightMap.put(matcher, weight);
    7476  }
    7577
    76   private Map<FeatureMatcher, Double> matcherToWeightMap = new HashMap<>();
     78  private final Map<FeatureMatcher, Double> matcherToWeightMap = new HashMap<>();
    7779
    7880  /**
    7981   * Searches a collection of candidate features for those that match the given
     
    9395
    9496  private Matches toMatches(Map<Feature, Double> featureToScoreMap, FeatureSchema schema) {
    9597    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);
    10099    return matches;
    101100  }
    102101
     
    111110
    112111  private Map<Feature, Double> featureToScoreMap(Map<FeatureMatcher, Matches> matcherToMatchesMap) {
    113112    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));
    118114    return featureToScoreMap;
    119115  }
    120116
    121117  private void addToFeatureToScoreMap(Matches matches, FeatureMatcher matcher,
    122118                                      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);
    126123    }
    127124  }
    128125
    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 
    135126  private double normalizedWeight(FeatureMatcher matcher) {
    136     return matcherToWeightMap.get(matcher).doubleValue() / weightTotal();
     127    return this.matcherToWeightMap.get(matcher) / this.weightTotal();
    137128  }
    138129
    139130  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();
    145132  }
    146133}
  • src/com/vividsolutions/jump/feature/AbstractBasicFeature.java

     
    182182        for (int i = 0; i < schema.getAttributeCount(); i++) {
    183183            if (schema.getAttributeType(i) == AttributeType.GEOMETRY) {
    184184                clone.setAttribute(i,
    185                     deep ? getGeometry().clone() : getGeometry());
     185                    deep ? getGeometry().copy() : getGeometry());
    186186            } else {
    187187                clone.setAttribute(i, getAttribute(i));
    188188            }
  • src/com/vividsolutions/jump/util/CoordinateArrays.java

     
    3434import java.util.ArrayList;
    3535import java.util.List;
    3636
    37 import org.locationtech.jts.algorithm.CGAlgorithms;
     37import org.locationtech.jts.algorithm.Orientation;
    3838import org.locationtech.jts.geom.Coordinate;
    3939import org.locationtech.jts.geom.Geometry;
    4040import org.locationtech.jts.geom.GeometryCollection;
     
    128128            Coordinate[] shell = poly.getExteriorRing().getCoordinates();
    129129
    130130            if (orientPolygons) {
    131                 shell = ensureOrientation(shell, CGAlgorithms.CLOCKWISE);
     131                shell = ensureOrientation(shell, Orientation.CLOCKWISE);
    132132            }
    133133
    134134            coordArrayList.add(shell);
     
    137137                Coordinate[] hole = poly.getInteriorRingN(i).getCoordinates();
    138138
    139139                if (orientPolygons) {
    140                     hole = ensureOrientation(hole, CGAlgorithms.COUNTERCLOCKWISE);
     140                    hole = ensureOrientation(hole, Orientation.COUNTERCLOCKWISE);
    141141                }
    142142
    143143                coordArrayList.add(hole);
     
    168168            return coord;
    169169        }
    170170
    171         int orientation = CGAlgorithms.isCCW(coord) ? CGAlgorithms.COUNTERCLOCKWISE
    172                                            : CGAlgorithms.CLOCKWISE;
     171        int orientation = Orientation.isCCW(coord) ? Orientation.COUNTERCLOCKWISE
     172                                           : Orientation.CLOCKWISE;
    173173
    174174        if (orientation != desiredOrientation) {
    175175            Coordinate[] reverse = coord.clone();
  • src/org/openstreetmap/josm/plugins/conflation/OsmFeature.java

     
    33package org.openstreetmap.josm.plugins.conflation;
    44
    55import java.util.Map;
     6import java.util.Objects;
    67
    78import org.openstreetmap.josm.data.osm.OsmPrimitive;
    89import org.openstreetmap.josm.plugins.jts.JTSConverter;
     
    910
    1011import com.vividsolutions.jump.feature.AbstractBasicFeature;
    1112import com.vividsolutions.jump.feature.AttributeType;
     13import com.vividsolutions.jump.feature.Feature;
    1214import com.vividsolutions.jump.feature.FeatureSchema;
    1315
    1416public class OsmFeature extends AbstractBasicFeature {
    1517    private Object[] attributes;
    16     private OsmPrimitive primitive;
    17     private JTSConverter converter;
     18    private final OsmPrimitive primitive;
    1819
    1920    /**
    2021     * Create a copy of the OSM geometry
     
    2223     */
    2324    public OsmFeature(OsmPrimitive prim, JTSConverter jtsConverter) {
    2425        super(new FeatureSchema());
    25         primitive = prim;
     26        this.primitive = Objects.requireNonNull(prim);
    2627        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;
    3335        if (jtsConverter != null)
    3436            converter = jtsConverter;
    3537        else
     
    6870        // objects with the same id
    6971        return (int) primitive.getUniqueId();
    7072    }
     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    }
    7198}
  • test/unit/com/vividsolutions/jcs/conflate/polygonmatch/WeightedMatcherTest.java

     
     1package com.vividsolutions.jcs.conflate.polygonmatch;
     2
     3import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
     4
     5import java.util.Arrays;
     6
     7import org.junit.jupiter.api.Test;
     8import org.junit.jupiter.api.extension.RegisterExtension;
     9import org.openstreetmap.josm.data.coor.LatLon;
     10import org.openstreetmap.josm.data.osm.Node;
     11import org.openstreetmap.josm.plugins.conflation.OsmFeature;
     12import org.openstreetmap.josm.plugins.jts.JTSConverter;
     13import org.openstreetmap.josm.testutils.JOSMTestRules;
     14import org.openstreetmap.josm.testutils.annotations.BasicPreferences;
     15
     16import com.vividsolutions.jump.feature.Feature;
     17import com.vividsolutions.jump.feature.FeatureCollection;
     18import com.vividsolutions.jump.feature.FeatureDataset;
     19import com.vividsolutions.jump.feature.FeatureSchema;
     20import com.vividsolutions.jump.feature.IndexedFeatureCollection;
     21
     22/**
     23 * Test class for {@link WeightedMatcher}
     24 * @author Taylor Smock
     25 */
     26@BasicPreferences
     27class 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

     
     1package org.openstreetmap.josm.plugins.conflation;
     2
     3import static org.junit.jupiter.api.Assertions.assertEquals;
     4import static org.junit.jupiter.api.Assertions.assertNotEquals;
     5import static org.junit.jupiter.api.Assertions.assertTrue;
     6
     7import java.util.Arrays;
     8
     9import org.junit.jupiter.api.Test;
     10import org.junit.jupiter.api.extension.RegisterExtension;
     11import org.openstreetmap.josm.data.coor.LatLon;
     12import org.openstreetmap.josm.data.osm.Node;
     13import org.openstreetmap.josm.data.osm.OsmPrimitive;
     14import org.openstreetmap.josm.plugins.jts.JTSConverter;
     15import org.openstreetmap.josm.testutils.JOSMTestRules;
     16import org.openstreetmap.josm.testutils.annotations.BasicPreferences;
     17
     18import com.vividsolutions.jump.feature.AttributeType;
     19import com.vividsolutions.jump.feature.FeatureSchema;
     20import nl.jqno.equalsverifier.EqualsVerifier;
     21
     22/**
     23 * Test class for {@link OsmFeature}
     24 * @author Taylor Smock
     25 */
     26@BasicPreferences
     27class 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}