Revision: 5878 http://sourceforge.net/p/jump-pilot/code/5878 Author: michaudm Date: 2018-06-17 12:10:51 +0000 (Sun, 17 Jun 2018) Log Message: ----------- Refactor Matching extension, use OJ aggregators, add code to svn
Added Paths: ----------- plug-ins/MatchingPlugIn/ plug-ins/MatchingPlugIn/trunk/ plug-ins/MatchingPlugIn/trunk/build.xml plug-ins/MatchingPlugIn/trunk/src/ plug-ins/MatchingPlugIn/trunk/src/fr/ plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/ plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/ plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/ plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/ plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/FeatureCollectionMatcher.java plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/I18NPlug.java plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/Index.java plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/Match.java plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/MatchEditingPlugIn.java plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/MatchMap.java plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/Matcher.java plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/MatcherRegistry.java plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/MatchingExtension.java plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/MatchingPlugIn.java plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/MatchingUpdatePlugIn.java plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/matcher/ plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/matcher/AbstractMatcher.java plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/matcher/AttributeMatcher.java plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/matcher/CentroidDistanceMatcher.java plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/matcher/DamarauLevenshteinDistanceMatcher.java plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/matcher/EqualsExactGeom2dMatcher.java plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/matcher/EqualsExactGeom3dMatcher.java plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/matcher/EqualsNormalizedGeom2dMatcher.java plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/matcher/EqualsNormalizedGeom3dMatcher.java plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/matcher/EqualsTopologicalGeomMatcher.java plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/matcher/EqualsWithCoordinateToleranceMatcher.java plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/matcher/GeometryMatcher.java plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/matcher/HausdorffDistanceMatcher.java plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/matcher/Intersects0DMatcher.java plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/matcher/Intersects1DMatcher.java plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/matcher/Intersects2DMatcher.java plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/matcher/IntersectsMatcher.java plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/matcher/IsWithinMatcher.java plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/matcher/LevenshteinDistanceMatcher.java plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/matcher/MatchAllAttributesMatcher.java plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/matcher/MatchAllMatcher.java plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/matcher/MatchAllStringsMatcher.java plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/matcher/MinimumDistanceMatcher.java plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/matcher/OverlappedByMatcher.java plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/matcher/OverlapsMatcher.java plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/matcher/SemiHausdorffDistanceMatcher.java plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/matcher/ShapeMatcher.java plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/matcher/StringEqualityIgnoreCaseAndAccentMatcher.java plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/matcher/StringEqualityIgnoreCaseMatcher.java plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/matcher/StringEqualityMatcher.java plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/matcher/StringMatcher.java plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/matching.properties plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/matching_fr.properties Added: plug-ins/MatchingPlugIn/trunk/build.xml =================================================================== --- plug-ins/MatchingPlugIn/trunk/build.xml (rev 0) +++ plug-ins/MatchingPlugIn/trunk/build.xml 2018-06-17 12:10:51 UTC (rev 5878) @@ -0,0 +1,76 @@ +<project name="matching" default="compile" basedir="."> + + <!--************************************************************************* + ***************************************************************************** + ** PROPERTIES ** + ***************************************************************************** + **************************************************************************--> + + <!-- PROPERTIES : MAIN ARCHITECTURE --> + <property name="src" value="src" /> + <property name="bin" value="bin" /> + <property name="lib" value="lib" /> + <property name="build" value="build" /> + <property name="dist" value="dist" /> + <property name="doc" value="doc" /> + <property name="resources" value="resources" /> + <property name="javadoc" value="javadoc" /> + + <property name="matching-version" value="0.8.0" /> + + <!-- =================================================================== --> + <!-- Defines the classpath used for compilation and test. --> + <!-- =================================================================== --> + <path id="classpath"> + <fileset dir="${lib}"> + <include name="**/*.jar"/> + </fileset> + </path> + + <target name="clean" id="clean"> + <delete dir="build"/> + <delete dir="${javadoc}"/> + </target> + + <target name="compile" id="compile" depends="clean"> + <tstamp/> + <mkdir dir="build"/> + <javac srcdir="${src}" destdir="build" + debug="on" deprecation="false" nowarn="true" + source="1.7" target="1.7"> + <!--compilerarg value="-Xlint:unchecked"/--> + <classpath refid="classpath"/> + </javac> + <copy todir="build"> + <fileset dir="${src}" includes="**/*.txt"/> + <fileset dir="${src}" includes="**/*.properties"/> + <fileset dir="${src}" includes="**/*.png"/> + <fileset dir="${src}" includes="**/*.gif"/> + <fileset dir="${src}" includes="**/*.jpg"/> + </copy> + </target> + + + <target name="matching-jar" id="matching-jar" depends="compile"> + <mkdir dir="${dist}"/> + <jar jarfile="${dist}/matching-${matching-version}.jar"> + <fileset dir="build"> + <include name="fr/michaelm/jump/plugin/match/**/*.class"/> + <include name="fr/michaelm/jump/plugin/match/**/*.properties"/> + </fileset> + </jar> + </target> + + <target name="matching-src" id="matching-src" depends="matching-jar"> + <mkdir dir="${dist}"/> + <zip zipfile="${dist}/matching-src-${matching-version}.zip"> + <fileset dir="${dist}"> + <include name="matching-${matching-version}.jar"/> + </fileset> + <fileset dir="."> + <include name="${src}/fr/michaelm/jump/plugin/match/**/*.java"/> + </fileset> + </zip> + </target> + +</project> \ No newline at end of file Added: plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/FeatureCollectionMatcher.java =================================================================== --- plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/FeatureCollectionMatcher.java (rev 0) +++ plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/FeatureCollectionMatcher.java 2018-06-17 12:10:51 UTC (rev 5878) @@ -0,0 +1,375 @@ +/* + * (C) 2017 Michaël Michaud + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + * For more information, contact: + * + * m.michael.mich...@orange.fr + */ + +package fr.michaelm.jump.plugin.match; + +import com.vividsolutions.jts.geom.Coordinate; +import com.vividsolutions.jts.geom.Envelope; +import com.vividsolutions.jts.geom.Geometry; +import com.vividsolutions.jts.index.strtree.STRtree; +import com.vividsolutions.jts.operation.union.UnaryUnionOp; +import com.vividsolutions.jump.feature.Feature; +import com.vividsolutions.jump.task.TaskMonitor; + +import fr.michaelm.jump.plugin.match.matcher.*; +import fr.michaelm.util.text.Rule; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; + +/** + * Matcher iterating through two FeatureCollection to find matching features. + * + * @author Michaël Michaud + */ +public class FeatureCollectionMatcher { + + private Collection<Feature> source; + private Collection<Feature> target; + private GeometryMatcher geometryMatcher; + private StringMatcher attributeMatcher; + private MatchMap matchMap; + private TaskMonitor monitor; + public boolean interrupted = false; + + // set n_m = true to try to match source features to several target + // features in one shot. + private boolean n_m = false; + private static final Matcher OVERLAP = OverlapsMatcher.instance(); + + /** + * A high level matcher able to compare features from two feature + * collections. It is able to compare pairs of features or to pre-process + * the feature collection in order to find N-M matches. + * @param source the source feature collection + * @param target the target feature collection + * @param geometryMatcher the Matcher to evaluate geometric similarity + * @param attributeMatcher the matcher to evaluate semantic similarity + */ + public FeatureCollectionMatcher(Collection<Feature> source, + Collection<Feature> target, + GeometryMatcher geometryMatcher, + StringMatcher attributeMatcher, + TaskMonitor monitor) { + if (geometryMatcher == MatchAllMatcher.MATCH_ALL) { + geometryMatcher = null; + } + if (attributeMatcher == MatchAllStringsMatcher.MATCH_ALL) { + attributeMatcher = null; + } + assert geometryMatcher != null || attributeMatcher != null : + "A FeatureCollectionMatcher must have at least one Matcher"; + this.source = source; + this.target = target; + this.geometryMatcher = geometryMatcher; + this.attributeMatcher = attributeMatcher; + this.monitor = monitor; + matchMap = new MatchMap(); + } + + /** + * Main method trying to match all features from both input feature + * collections and returning the set of source features matching one or + * several target features. + * @param singleSource whether a target Feature can be matched by several + * source features or not. + * @param singleTarget whether a source feature can match several target + * features or not. + */ + public Collection<Feature> matchAll(boolean singleSource, + boolean singleTarget) throws Exception { + long t0 = System.currentTimeMillis(); + if (geometryMatcher != null) { + System.out.println("Geometry Matching"); + monitor.report("Geometry matching"); + matchMap = geometryMatching(singleSource, singleTarget); + } + if (attributeMatcher != null) { + System.out.println("Semantic Matching"); + monitor.report("Attribute matching"); + matchMap = attributeMatching(singleSource, singleTarget); + } + if (geometryMatcher == null && attributeMatcher == null) { + throw new Exception("Invalid params (both geometric and attribute matchers are null !)"); + } + //System.out.println("MatchMap before filter : \n" + matchMap.toString().replaceAll(",","\n")); + monitor.report("Filtering results"); + matchMap = matchMap.filter(singleSource, singleTarget); + //System.out.println("MatchMap after filter : \n" + matchMap.toString().replaceAll(",","\n")); + System.out.println("Match performed in " + (System.currentTimeMillis()-t0) + " ms"); + return matchMap.getSourceFeatures(); + } + + public MatchMap getMatchMap() { + return matchMap; + } + + public void clearMatchMap() { + matchMap.clear(); + } + + + /** + * Returns a MatchMap representing all the match scores obtained by + * comparing source feature geometries with target feature geometries with + * the GeometryMatcher. + * @param singleSource whether a target Feature can be matched by several + * source features or not. + * @param singleTarget whether a source feature can match several target + * features or not. + */ + public MatchMap geometryMatching(boolean singleSource, boolean singleTarget) throws Exception { + double maxDistance = geometryMatcher.getMaximumDistance(); + if (Double.isNaN(maxDistance)) maxDistance = 0.0; + //System.out.println("Geometry Matching " + geometryMatcher + " " + maxDistance); + long t0 = System.currentTimeMillis(); + double minOverlapping = geometryMatcher.getMinimumOverlapping(); + //System.out.println("geometryMatcher.minOverlapping = " + minOverlapping); + monitor.report("Geometry matching : indexing features"); + STRtree index = indexFeatureCollection(target); + // For each feature of the source collection + monitor.report("Geometry matching : matching feature geometries"); + int countf1 = 0; + int total = source.size(); + for (Feature f1 : source) { + //System.out.println("Feature " + f1.getID()); + Geometry g1 = f1.getGeometry(); + Envelope env = new Envelope(g1.getEnvelopeInternal()); + env.expandBy(maxDistance); + List<Feature> candidates = index.query(env); + // if matching_layer = reference_layer don't try to match f1 with itself + candidates.remove(f1); + // This loop can select several target features for one source + // feature, a singleTarget filter must be applied afterwards + int countf2 = 0; + // if multiple targets are authorized, a oneOneMatches map is built + // during the one-to-one match phase in order to be used and optimize + // the phase where we try to match source with union of candidates. + Map<Feature,Match> oneOneMatches = null; + if (!singleTarget) oneOneMatches = new HashMap<Feature,Match>(); + for (Feature f2 : candidates) { + double score = geometryMatcher.match(f1, f2, null); + if (score > 0.0) { + Match match = new Match(f1, f2, score); + matchMap.add(match); + if (!singleTarget) oneOneMatches.put(f2, match); + countf2++; + } + } + + // If one source can match multiple target + // and several target candidates are available + // and some candidates have not been individually matched + if (!singleTarget && candidates.size() > 1 && !(countf2 == candidates.size())) { + Geometry globalTarget = union(candidates); + // if g1 matches the union of candidates, we try to attribute + // a score to each g1/candidate pair + if (geometryMatcher.match(g1, globalTarget, null) > 0) { + Geometry g1Buffer = g1.buffer(maxDistance, 4); + // if g1 matches union of g2, we put all g1/g2 matches + // in a temporary structure ordered by match scores + Set<Match> partialMatches = new TreeSet<Match>(); + for (Feature f2 : candidates) { + Geometry g2Buffer = f2.getGeometry().buffer(maxDistance, 4); + Geometry intersection = g1Buffer.intersection(g2Buffer); + if (intersection.isEmpty()) continue; + double ratio1 = intersection.getArea()/g1Buffer.getArea(); + double ratio2 = intersection.getArea()/g2Buffer.getArea(); + if (ratio1 > 0.01) { + // we set the ratio of the temporary match to the + // max of ratio1 and ratio 2 (match is good if f1 + // buffer covers a lrage part of f2 or if f2 buffer + // covers a large part of f1 + partialMatches.add(new Match(f1, f2, Math.max(ratio1, ratio2))); + } + } + int countPartialMatches = 0; + // Test temporary matches from the best score to the worst, + // and add them to the final matchMap until f1 is completely + // covered by f2 buffers + SortedSet<Match> previousMatches = matchMap.getMatchesForSourceFeature(f1); + for (Match match : partialMatches) { + Match oneOneMatch = oneOneMatches.get(match.getTarget()); + if (oneOneMatch != null) { + if (oneOneMatch.getScore() > match.getScore()) { + continue; + } + } + // add at least one match + if (0 == countPartialMatches) { + if (oneOneMatch != null) matchMap.removeMatch(oneOneMatch); + matchMap.add(match); + } + else { + // substract candidate buffer from f1 + Geometry diff = homogeneousDifference(g1, match.getTarget().getGeometry().buffer(maxDistance, 4)); + // Add the match if the diff operation modified original geometry + if (!diff.equals(g1)) { + matchMap.add(match); + } + // break if f1 is completely covered by candidate buffers + if (diff.isEmpty()) break; + else g1 = diff; + } + countPartialMatches++; + } + } + } + if (monitor.isCancelRequested()) { + interrupted = true; + return matchMap; + }; + monitor.report(++countf1, total, "features"); + } + System.out.println("Direct Geometry Matching done in " + (System.currentTimeMillis()-t0) + " ms"); + return matchMap; + } + + private Geometry homogeneousDifference(Geometry g1, Geometry g2) { + Geometry g = g1.difference(g2); + if (g.isEmpty()) return g; + else if (g.getNumGeometries() == 1) return g; + else if (g.getDimension() < g1.getDimension()) { + if (g1.getDimension() == 0) return g1.getFactory().createPoint((Coordinate)null); + if (g1.getDimension() == 1) return g1.getFactory().createLineString(new Coordinate[0]); + if (g1.getDimension() == 2) return g1.getFactory().createPolygon(g1.getFactory().createLinearRing(new Coordinate[0]), null); + } + else { + List<Geometry> list = new ArrayList<Geometry>(); + for (int i = 0 ; i < g.getNumGeometries() ; i++) { + if (g.getGeometryN(i).getDimension() == g1.getDimension()) { + list.add(g.getGeometryN(i)); + } + } + return g1.getFactory().buildGeometry(list); + } + return g; + } + + private STRtree indexFeatureCollection(Collection<Feature> collection) { + STRtree index = new STRtree(); + for (Feature f : collection) { + index.insert(f.getGeometry().getEnvelopeInternal(), f); + } + return index; + } + + private SortedMap<String,Collection<Feature>> indexFeatureCollection(Collection<Feature> collection, String attribute) { + SortedMap<String,Collection<Feature>> map = new TreeMap<String,Collection<Feature>>(); + for (Feature f : collection) { + String value = f.getString(attribute); + Collection coll = map.get(value); + if (coll == null) { + coll = new ArrayList<Feature>(); + map.put(value, coll); + } + coll.add(f); + } + return map; + } + + private Geometry union(List<Feature> features) { + List geom = new ArrayList(); + for (Feature f : features) geom.add(f.getGeometry()); + return UnaryUnionOp.union(geom); + } + + private MatchMap attributeMatching(boolean singleSource, boolean singleTarget) throws Exception { + String sourceAttribute = attributeMatcher.getSourceAttribute(); + String targetAttribute = attributeMatcher.getTargetAttribute(); + Rule sourceRule = attributeMatcher.getSourceRule(); + Rule targetRule = attributeMatcher.getTargetRule(); + // If geometryMatcher is null, a simple join will be done. + if (geometryMatcher == null && attributeMatcher != null) { + monitor.report("Attribute matching : indexing features"); + Index index = attributeMatcher.createIndex(target); + int count = 0; + int total = source.size(); + monitor.report("Attribute matching : matching feature attributes"); + for (Feature f1 : source) { + String sourceValue = sourceRule.transform(f1.getString(sourceAttribute)); + //System.out.println("sourceValue : " + sourceValue); + Set<Feature> candidates = + index.query(sourceValue); + if (candidates == null || candidates.isEmpty()) { + continue; + } + else if (Double.isNaN(attributeMatcher.getMaximumDistance())) { + for (Feature f2 : candidates) { + matchMap.add(new Match(f1, f2, 1.0)); + } + } + // In the case where a BKTree is used, there is room for + // optimizition because distances are already computed by the + // BKTree query method + else { + for (Feature f2 : candidates) { + double d = attributeMatcher.match(f1, f2, null); + matchMap.add(new Match(f1, f2, d)); + } + } + if (monitor.isCancelRequested()) { + interrupted = true; + return matchMap; + }; + monitor.report(++count, total, "features"); + } + // index attribute data + } + // If a geometry matching has already been done, attribute matching + // use the resulting MatchMap from the geometry matching process + else { + List<Match> new_matches = new ArrayList<Match>(); + Set<Match> allMatches = matchMap.getAllMatches(); + int count = 0; + int total = allMatches.size(); + for (Match m : allMatches) { + String srcA = sourceRule.transform(m.getSource().getString(sourceAttribute)); + String tgtA = targetRule.transform(m.getTarget().getString(targetAttribute)); + double newScore = m.combineScore(attributeMatcher.match(srcA, tgtA, null)); + if (newScore > 0.0) { + new_matches.add(new Match(m.getSource(), m.getTarget(), newScore)); + } + if (monitor.isCancelRequested()) { + interrupted = true; + return matchMap; + }; + monitor.report(++count, total, "matches"); + } + matchMap.clear(); + for (Match match : new_matches) { + matchMap.add(match); + } + + } + return matchMap; + } + +} Added: plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/I18NPlug.java =================================================================== --- plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/I18NPlug.java (rev 0) +++ plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/I18NPlug.java 2018-06-17 12:10:51 UTC (rev 5878) @@ -0,0 +1,76 @@ +/* + * (C) 2017 Michaël Michaud + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + * For more information, contact: + * + * m.michael.mich...@orange.fr + */ + +package fr.michaelm.jump.plugin.match; + +import java.text.MessageFormat; +import java.util.Locale; +import java.util.MissingResourceException; +import java.util.ResourceBundle; + +import org.apache.log4j.Logger; + +import com.vividsolutions.jump.I18N; + +public class I18NPlug { + + private static final Logger LOG = Logger.getLogger(I18NPlug.class); + + // Use the same locale as the main program + private static final ResourceBundle I18N_RESOURCE = + ResourceBundle.getBundle("fr/michaelm/jump/plugin/match/matching", new Locale(I18N.getLocale())); + + public static String getI18N(String key) { + try { return I18N_RESOURCE.getString(key); } + catch (MissingResourceException ex) { + String[] labelpath = key.split("\\."); + ex.printStackTrace(); + return labelpath[labelpath.length-1]; + } + catch (Exception ex) { + ex.printStackTrace(); + return ""; + } + } + + /** + * Get a formatted message with argument insertion. + * + * @param label with argument insertion : {0} + * @param objects values to insert in the message + * @return i18n label + */ + public static String getMessage(final String label, final Object[] objects) { + try { + final MessageFormat mformat = new MessageFormat(I18N_RESOURCE.getString(label)); + return mformat.format(objects); + } catch (java.util.MissingResourceException e) { + final String[] labelpath = label.split("\\."); + LOG.warn(e.getMessage() + " no default value, the resource key is used: " + + labelpath[labelpath.length - 1]); + final MessageFormat mformat = new MessageFormat( + labelpath[labelpath.length - 1]); + return mformat.format(objects); + } + } + +} \ No newline at end of file Added: plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/Index.java =================================================================== --- plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/Index.java (rev 0) +++ plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/Index.java 2018-06-17 12:10:51 UTC (rev 5878) @@ -0,0 +1,38 @@ +/* + * (C) 2017 Michaël Michaud + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + * For more information, contact: + * + * m.michael.mich...@orange.fr + */ + +package fr.michaelm.jump.plugin.match; + +import com.vividsolutions.jump.feature.Feature; +import java.util.Set; + +/** + * Index returns a Set of Feature candidates from an criteria Object. + * Criteria is tipically a Geometry, an Enveloppe or an attribute value. + * + * @author Michaël Michaud + */ +public interface Index { + + Set<Feature> query(Object o); + +} Added: plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/Match.java =================================================================== --- plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/Match.java (rev 0) +++ plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/Match.java 2018-06-17 12:10:51 UTC (rev 5878) @@ -0,0 +1,112 @@ +/* + * (C) 2017 Michaël Michaud + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + * For more information, contact: + * + * m.michael.mich...@orange.fr + */ + +package fr.michaelm.jump.plugin.match; + +import com.vividsolutions.jump.feature.Feature; + +/** + * A match between two features. A match is oriented from a source feature + * to a target feature. + * + * @author Michaël Michaud + */ +public class Match implements Comparable<Match> { + + private Feature source; + private Feature target; + private double score; + //private double minDistance = Double.NaN; + + /** + * Create a Match object. + * @param source the source Feature to match from + * @param target the target Feature to match to + * @param score the score of the match + */ + public Match(Feature source, Feature target, double score) { + this.source = source; + this.target = target; + this.score = score; + } + + public Feature getSource() { + return source; + } + + public Feature getTarget() { + return target; + } + + public double getScore() { + return score; + } + + /** + * Combine score with another score so that + * - one of to scores is 0 -> final score is 0 + * - both score are 1 -> final score is 1 + */ + public double combineScore(double otherScore) { + return score * otherScore; + //return this; + } + + /** + * Compare two matches by comparing their matching score first, then, in + * case of matching score equality, their source feature ID, and in case + * of source feature ID equality, their target ID. + */ + public int compareTo(Match m) { + if (getScore() > m.getScore()) return -1; + else if (getScore() < m.getScore()) return 1; + else { + if (getSource().getID() < m.getSource().getID()) return -1; + else if (getSource().getID() > m.getSource().getID()) return 1; + else { + if (getTarget().getID() < m.getTarget().getID()) return -1; + else if (getTarget().getID() > m.getTarget().getID()) return 1; + else return 0; + } + } + } + + /** + * Two matches are equal iff their source feature ID, their target feature + * ID AND their matching score are equal. + * It is important that equals is consistent with compareTo + */ + public boolean equals(Object o) { + if (o instanceof Match) { + Match other = (Match)o; + return source.getID() == other.getSource().getID() && + target.getID() == other.getTarget().getID() && + score == other.getScore(); + } + return false; + } + + public String toString() { + return "Match " + source.getID() + " and " + target.getID() + " with score " + score; + } + +} Added: plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/MatchEditingPlugIn.java =================================================================== --- plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/MatchEditingPlugIn.java (rev 0) +++ plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/MatchEditingPlugIn.java 2018-06-17 12:10:51 UTC (rev 5878) @@ -0,0 +1,257 @@ +/* + * (C) 2017 Michaël Michaud + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + * For more information, contact: + * + * m.michael.mich...@orange.fr + */ + +package fr.michaelm.jump.plugin.match; + +import com.vividsolutions.jts.geom.Coordinate; +import com.vividsolutions.jts.geom.GeometryFactory; +import com.vividsolutions.jump.feature.BasicFeature; +import com.vividsolutions.jump.feature.Feature; +import com.vividsolutions.jump.feature.FeatureCollection; +import com.vividsolutions.jump.feature.FeatureSchema; +import com.vividsolutions.jump.workbench.WorkbenchContext; +import com.vividsolutions.jump.workbench.model.Layer; +import com.vividsolutions.jump.workbench.model.LayerManager; +import com.vividsolutions.jump.workbench.plugin.EnableCheck; +import com.vividsolutions.jump.workbench.plugin.MultiEnableCheck; +import com.vividsolutions.jump.workbench.plugin.PlugInContext; +import com.vividsolutions.jump.workbench.plugin.AbstractPlugIn; +import com.vividsolutions.jump.workbench.ui.GUIUtil; +import com.vividsolutions.jump.workbench.ui.MenuNames; +import com.vividsolutions.jump.workbench.ui.MultiInputDialog; +import com.vividsolutions.jump.workbench.ui.WorkbenchFrame; +import com.vividsolutions.jump.workbench.ui.task.TaskMonitorManager; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.Collection; +import java.util.Iterator; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JOptionPane; +import javax.swing.JTextField; + +/** + * PlugIn to find features from a layer matching features of another layer. + * @author Michaël Michaud + */ +public class MatchEditingPlugIn extends AbstractPlugIn implements ActionListener { + + private final String MATCH_EDITING = I18NPlug.getI18N("Match-editing"); + private final String MATCHING = I18NPlug.getI18N("Matching"); + private final String LINK_LAYER = I18NPlug.getI18N("Links"); + private final String SOURCE_LAYER = I18NPlug.getI18N("Source-layer"); + private final String TARGET_LAYER = I18NPlug.getI18N("Target-layer"); + + private WorkbenchContext workbenchContext = null; + private Layer linkLayer = null; + private Layer sourceLayer = null; + private Layer targetLayer = null; + + + + public MatchEditingPlugIn() { + } + + public String getName() { + return MATCH_EDITING; + } + + public void initialize(PlugInContext context) throws Exception { + + context.getFeatureInstaller().addMainMenuPlugin( + this, new String[]{MenuNames.PLUGINS, MATCHING}, + MATCH_EDITING + "...", + false, null, getEnableCheck(context)); + workbenchContext = context.getWorkbenchContext(); + } + + /** + * Execute method initialize the plugin interface and get all the + * parameters from the user. + */ + public boolean execute(PlugInContext context) throws Exception { + + //////////////////////////////////////////////////////////////////////// + // UI : CREATE MULTITAB INPUT DIALOG + //////////////////////////////////////////////////////////////////////// + + final MultiInputDialog dialog = new MultiInputDialog( + context.getWorkbenchFrame(), MATCH_EDITING, false); + + linkLayer = context.getLayerNamePanel().getSelectedLayers()[0]; + FeatureCollection fc = linkLayer.getFeatureCollectionWrapper(); + JTextField linkTextField = dialog.addTextField(LINK_LAYER, linkLayer.getName(), 24, null, null); + linkTextField.setEditable(false); + + sourceLayer = findTargetLayer(linkLayer, "SOURCE", context.getLayerManager()); + if (sourceLayer != null) { + JTextField sourceTextField = dialog.addTextField(SOURCE_LAYER, sourceLayer.getName(), 24, null, null); + sourceTextField.setEditable(false); + } else { + JOptionPane.showMessageDialog(context.getWorkbenchFrame(), + I18NPlug.getI18N("Missing-source-layer"), + I18NPlug.getI18N("Missing-layer"), JOptionPane.ERROR_MESSAGE); + return false; + } + + + targetLayer = findTargetLayer(linkLayer, "TARGET", context.getLayerManager()); + if (targetLayer != null) { + JTextField targetTextField = dialog.addTextField(TARGET_LAYER, targetLayer.getName(), 24, null, null); + targetTextField.setEditable(false); + } else { + JOptionPane.showMessageDialog(context.getWorkbenchFrame(), + I18NPlug.getI18N("Missing-target-layer"), + I18NPlug.getI18N("Missing-layer"), JOptionPane.ERROR_MESSAGE); + return false; + } + + + JButton createLinksButton = dialog.addButton(I18NPlug.getI18N("Create-links-label"), I18NPlug.getI18N("Create-links"), ""); + createLinksButton.addActionListener(this); + createLinksButton.setActionCommand("link"); + + JButton updateMatchesButton = dialog.addButton(I18NPlug.getI18N("Update-matches"), I18NPlug.getI18N("Update-matches"), ""); + updateMatchesButton.addActionListener(this); + updateMatchesButton.setActionCommand("update"); + + dialog.setSideBarDescription(I18NPlug.getI18N("Match-editing-description")); + + for (Object o : workbenchContext.getLayerManager().getLayers()) { + Layer layer = (Layer)o; + if (layer != linkLayer && layer != sourceLayer && layer != targetLayer) { + layer.setSelectable(false); + layer.setEditable(false); + layer.setVisible(false); + } + } + linkLayer.setVisible(true); + linkLayer.setEditable(true); + linkLayer.setSelectable(true); + + sourceLayer.setVisible(true); + sourceLayer.setSelectable(true); + sourceLayer.setEditable(false); + + targetLayer.setVisible(true); + targetLayer.setSelectable(true); + targetLayer.setEditable(false); + + GUIUtil.centreOnWindow(dialog); + dialog.setVisible(true); + + return true; + + } + + + + + // Return the layer containing features identified by attribute column + // return null if layer is not found + private Layer findTargetLayer(Layer links, String attribute, LayerManager layerManager) { + int index = links.getFeatureCollectionWrapper().getFeatureSchema().getAttributeIndex(attribute); + if (index < 0) return null; + for (Iterator it = links.getFeatureCollectionWrapper().iterator() ; it.hasNext() ;) { + Feature f = (Feature)it.next(); + int fid = f.getInteger(index); + for (Object o : layerManager.getLayers()) { + if (o == links) continue; + Layer lyr = (Layer)o; + for (Iterator it2 = lyr.getFeatureCollectionWrapper().iterator() ; it2.hasNext() ;) { + Feature f2 = (Feature)it2.next(); + if (fid == f2.getID()) { + return lyr; + } + } + } + } + return null; + } + + private EnableCheck getEnableCheck(final PlugInContext context) { + return new MultiEnableCheck() + .add(context.getCheckFactory().createTaskWindowMustBeActiveCheck()) + .add(context.getCheckFactory().createExactlyNLayersMustBeSelectedCheck(1)) + .add(context.getCheckFactory().createAtLeastNLayersMustExistCheck(3)) + .add(context.getCheckFactory().createSelectedLayersMustBeEditableCheck()) + .add(new EnableCheck(){ + public String check(JComponent component) { + Layer lyr = context.getWorkbenchContext().getLayerableNamePanel().getSelectedLayers()[0]; + FeatureSchema schema = lyr.getFeatureCollectionWrapper().getFeatureSchema(); + return schema.hasAttribute("SOURCE") && + schema.hasAttribute("TARGET") && + schema.hasAttribute("SCORE") ? + null : I18NPlug.getI18N("Invalid-link-layer"); + } + }); + } + + public void actionPerformed(ActionEvent e) { + if ("link".equals(e.getActionCommand())) { + Collection selectedSource = workbenchContext + .getLayerViewPanel() + .getSelectionManager() + .getFeaturesWithSelectedItems(sourceLayer); + Collection selectedTarget = workbenchContext + .getLayerViewPanel() + .getSelectionManager() + .getFeaturesWithSelectedItems(targetLayer); + for (Object o1 : selectedSource) { + Feature source = (Feature)o1; + for (Object o2 : selectedTarget) { + Feature target = (Feature)o2; + BasicFeature bf = new BasicFeature(linkLayer.getFeatureCollectionWrapper().getFeatureSchema()); + Coordinate cSource = source.getGeometry().getInteriorPoint().getCoordinate(); + Coordinate cTarget = target.getGeometry().getInteriorPoint().getCoordinate(); + if (cSource.equals(cTarget)) { + bf.setGeometry(new GeometryFactory().createPoint(cSource)); + } + else { + bf.setGeometry(new GeometryFactory().createLineString( + new Coordinate[]{cSource, cTarget})); + } + bf.setAttribute("SOURCE", source.getID()); + bf.setAttribute("TARGET", target.getID()); + bf.setAttribute("SCORE", 1.0); + linkLayer.getFeatureCollectionWrapper().add(bf); + } + } + } + else if ("update".equals(e.getActionCommand())) { + try { + MatchingUpdatePlugIn mupi = new MatchingUpdatePlugIn(); + mupi.setLinkLayerName(linkLayer.getName()); + mupi.setSourceLayerName(sourceLayer.getName()); + mupi.setTargetLayerName(targetLayer.getName()); + PlugInContext context = workbenchContext.createPlugInContext(); + if (mupi.execute(context)) { + new TaskMonitorManager().execute(mupi, context); + } + } catch(Exception ex) { + WorkbenchFrame.toMessage(ex); + } + } + } + +} Added: plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/MatchMap.java =================================================================== --- plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/MatchMap.java (rev 0) +++ plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/MatchMap.java 2018-06-17 12:10:51 UTC (rev 5878) @@ -0,0 +1,230 @@ +/* + * (C) 2017 Michaël Michaud + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + * For more information, contact: + * + * m.michael.mich...@orange.fr + */ + +package fr.michaelm.jump.plugin.match; + +import com.vividsolutions.jump.feature.Feature; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + +/** + * A Map accumulating information about matches between two sets of features. + * + * The MatchMap will store every single Match in a Tree Map ordering all + * possible matches from the best score to the worst score. For matches + * returning the same score, ordering is determined by the + * {@link Match#compareTo(Match other)} method. + * + * @author Michaël Michaud + */ +public class MatchMap { + + private final SortedSet<Match> EMPTY_SET = Collections.unmodifiableSortedSet(new TreeSet<Match>()); + + private final Map<Feature,TreeSet<Match>> sourceMap = new HashMap<Feature,TreeSet<Match>>(); + private final Map<Feature,TreeSet<Match>> targetMap = new HashMap<Feature,TreeSet<Match>>(); + + //TODO : to improve performance, instead of maintaining 3 ordered map during + // the feeding (at each add call), sort the map on demand, keeping track of + // the ordering state (sorted after a get* or a filter call, unsorted after + // a add call. + boolean sorted; + + /** + * Construct a new MatchMap. + */ + public MatchMap() {} + + /** + * Add a match to this MatchMap. + * In version 0.6.0+, we assume that feature 1 and feature 2 can have have + * one match only (so that the test to keep the best match only is removed) + */ + public void add(Match m) { + TreeSet<Match> set = sourceMap.get(m.getSource()); + if (set == null) { + set = new TreeSet<>(); + sourceMap.put(m.getSource(), set); + } + set.add(m); + set = targetMap.get(m.getTarget()); + if (set == null) { + set = new TreeSet<>(); + targetMap.put(m.getTarget(), set); + } + set.add(m); + } + + /** + * Get the whole match Set. + */ + public Set<Match> getAllMatches() { + Set<Match> matches = new HashSet<Match>(); + for (Feature feature : sourceMap.keySet()) matches.addAll(sourceMap.get(feature)); + return matches; + } + + /** + * Get the set of features matching one or more features. + */ + public Set<Feature> getSourceFeatures() { + return sourceMap.keySet(); + } + + /** + * Get the set of features being matched by one or more features. + */ + public Set<Feature> getTargetFeatures() { + return targetMap.keySet(); + } + + /** + * Get Matches recorded for this source Feature. + */ + public SortedSet<Match> getMatchesForSourceFeature(Feature f) { + SortedSet<Match> matches = sourceMap.get(f); + return matches == null ? EMPTY_SET : matches; + } + + /** + * Get Matches recorded for this target Feature. + */ + public SortedSet<Match> getMatchesForTargetFeature(Feature f) { + SortedSet<Match> matches = targetMap.get(f); + return matches == null ? EMPTY_SET : matches; + } + + /** + * Get Features matching source Feature f. + */ + public List<Feature> getMatchedFeaturesFromSource(Feature f) { + TreeSet<Match> matchedFeatures = sourceMap.get(f); + List<Feature> list = new ArrayList<>(); + if (matchedFeatures == null) return list; + for (Match m : matchedFeatures) { + list.add(m.getTarget()); + } + return list; + } + + /** + * Get Features matching target Feature f. + */ + public List<Feature> getMatchedFeaturesFromTarget(Feature f) { + TreeSet<Match> matchedFeatures = targetMap.get(f); + List<Feature> list = new ArrayList<>(); + if (matchedFeatures == null) return list; + for (Match m : matchedFeatures) { + list.add(m.getSource()); + } + return list; + } + + /** + * Return Match from source to target. Usually, the result contains 0 + * or 1 Match, but nothing prevent insertion of several matches per couple + * of features. + */ + public SortedSet<Match> getMatches(Feature source, Feature target) { + // Set of matches from f1 + TreeSet<Match> set1 = sourceMap.get(source); + // Set of matches to f2 + TreeSet<Match> set2 = targetMap.get(target); + // Intersection of both sets = Match:f1->f2 + if (set1 != null && set2 != null) { + SortedSet<Match> set = (TreeSet<Match>)set1.clone(); + set.retainAll(set2); + return set; + } + else return EMPTY_SET; + } + + private boolean removeMatchesForSourceFeature(Feature f) { + TreeSet<Match> set = sourceMap.remove(f); + return /*matches.removeAll(set)*/ true; + } + + private boolean removeMatchesForTargetFeature(Feature f) { + TreeSet<Match> set = targetMap.remove(f); + return /*matches.removeAll(set)*/ true; + } + + /** + * Remove a match from the map. + */ + public void removeMatch(Match m, boolean singleSource, boolean singleTarget) { + if (singleTarget) removeMatchesForSourceFeature(m.getSource()); + if (singleSource) removeMatchesForTargetFeature(m.getTarget()); + } + + /** + * Remove a match from the map. + */ + public void removeMatch(Match m) { + // remove match from sourceMap + sourceMap.get(m.getSource()).remove(m); + // if sourceMap has no more match for this source, remove source feature + if (sourceMap.get(m.getSource()).size() == 0) sourceMap.remove(m.getSource()); + // remove match from targetMap + targetMap.get(m.getTarget()).remove(m); + // if targetMap has no more match for this target, remove target feature + if (targetMap.get(m.getTarget()).size() == 0) targetMap.remove(m.getTarget()); + } + + /** + * Filter the matchMap so that each source feature has only one target match + * and/or each target feature has only one source match. + */ + public MatchMap filter(boolean singleSource, boolean singleTarget) { + if (!singleSource && !singleTarget) return this; + //TreeSet<Match> filteredMatches = new TreeSet<Match>(); + // new code + MatchMap matchMap = new MatchMap(); + TreeSet<Match> matches = new TreeSet<Match>(); + for (Feature feature : sourceMap.keySet()) matches.addAll(sourceMap.get(feature)); + for (Match match : matches) { + Feature source = match.getSource(); + Feature target = match.getTarget(); + // Check if matchMap already has target features for this source + SortedSet<Match> matchesForSource = matchMap.getMatchesForSourceFeature(source); + // Check if matchMap already has source features for this target + SortedSet<Match> matchesForTarget = matchMap.getMatchesForTargetFeature(target); + if (singleTarget && matchesForSource.size() > 0) continue; + else if (singleSource && matchesForTarget.size() > 0) continue; + else matchMap.add(match); + } + return matchMap; + } + + public void clear() { + sourceMap.clear(); + targetMap.clear(); + } + +} Added: plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/Matcher.java =================================================================== --- plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/Matcher.java (rev 0) +++ plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/Matcher.java 2018-06-17 12:10:51 UTC (rev 5878) @@ -0,0 +1,125 @@ +/* + * (C) 2017 Michaël Michaud + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + * For more information, contact: + * + * m.michael.mich...@orange.fr + */ + +package fr.michaelm.jump.plugin.match; + +import com.vividsolutions.jump.feature.Feature; + +/** + * Interface for all simple matchers able to evaluate if a feature f matches + * a reference feature ref and how well it matches. + * Matcher is not symmetric. For example, IncludeMatcher will return 1 for + * match(f, ref) if f is included in ref, and 0 for match(ref, f). + * On the other hand, minimum distance should be symmetric. + * @author Michaël Michaud + */ +public interface Matcher { + + //String MAXIMUM_DISTANCE = I18NPlug.getI18N("Maximum-distance"); + + //String MINIMUM_OVERLAPPING = I18NPlug.getI18N("Minimum-overlapping"); + + /** + * Returns a distance measuring the match quality of Feature f with a + * reference Feature ref. + * The method returns 0 if f and ref do not match at all, and 1 if they + * match perfectly. + * If (f == ref), match should always return 1, but the return value of + * match(f, ref, context) if f.equals(ref) depends of the exact semantic + * of the matcher. + * It is not required that match(f, ref, context) = match(ref, f, context). + * + * @param f Feature to match from + * @param ref reference Feature to match to + * @param context object containing useful information to check if + * Feature f effectively matches Feature ref + * + * @return a double in the range [0-1] representative of the match quality + * between f and ref. + * + * @throws Exception if input data cannot be processed. + */ + double match(Feature f, Feature ref, Object context) throws Exception; + + + /** + * Returns the maximum distance accepted between f1 and ref + * Exact meaning highly depends on what distance is measured, but whatever + * the definition is (minimum distance, hausdorff distance, levenshtein + * distance...), if distance between f and ref is over the maximum + * distance, the value returned by match method will be 0. + * <ul> + * <li> + * If 0 is returned, match will always return 0 except for two + * identical features (identical meaning depends on matcher definition) + * </li> + * <li> + * Returning NaN means that using a tolerance has no meaning for this + * matcher (example, getMaximumDistance of equals matchers returns NaN). + * </li> + * <li> + * If Double.POSITIVE_INFINITY is returned, matches between f and ref will + * alway return a non null value. + * </li> + * </ul> + */ + double getMaximumDistance(); + + + /** + * Returns the minimum overlapping between f and ref, where overlapping is + * generally expressed as a percentage, but not necessarily. + * Overlapping may have different meanings as the ratio between common area + * and f area or between the length of the longest common substring of two + * attributes values and the length of the full string.<br> + * Depending on the Matcher, overlapping between f and ref may be + * directional (ex. common area / f area) or symmetric (intersection area + * / union area). + * <ul> + * <li> + * If 0 is returned, any pair of f and ref which intersects will return a + * non null value. + * </li> + * <li> + * NaN means that this criteria has no meaning for this matcher. + * </li> + * <li> + * 100.0 generally means that f and ref must be equal, but the precise + * definition may bary from a matcher to another. + * </li> + * </ul> + */ + double getMinimumOverlapping(); + + /** + * Sets the maximum distance returning a non null value. + * @see #getMaximumDistance + */ + void setMaximumDistance(double max_dist); + + /** + * Sets the minimum overlapping ratio returning a non null value. + * @see #getMinimumOverlapping + */ + void setMinimumOverlapping(double min_overlap); + +} Added: plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/MatcherRegistry.java =================================================================== --- plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/MatcherRegistry.java (rev 0) +++ plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/MatcherRegistry.java 2018-06-17 12:10:51 UTC (rev 5878) @@ -0,0 +1,93 @@ +/* + * (C) 2017 Michaël Michaud + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + * For more information, contact: + * + * m.michael.mich...@orange.fr + */ + +package fr.michaelm.jump.plugin.match; + +import fr.michaelm.jump.plugin.match.matcher.*; +import java.util.LinkedHashMap; +import java.util.Map; + + +/** + * Matcher Registry + * @author Michaël Michaud + */ +public class MatcherRegistry<T extends Matcher> { + + public static MatcherRegistry<GeometryMatcher> GEOMETRY_MATCHERS = new MatcherRegistry<> ( + MatchAllMatcher.instance(), + EqualsExactGeom3dMatcher.instance(), + EqualsNormalizedGeom3dMatcher.instance(), + EqualsExactGeom2dMatcher.instance(), + EqualsNormalizedGeom2dMatcher.instance(), + EqualsTopologicalGeomMatcher.instance(), + EqualsWithCoordinateToleranceMatcher.instance(), + + IsWithinMatcher.instance(), + OverlapsMatcher.instance(), + OverlappedByMatcher.instance(), + + IntersectsMatcher.instance(), + Intersects0DMatcher.instance(), + Intersects1DMatcher.instance(), + Intersects2DMatcher.instance(), + + MinimumDistanceMatcher.instance(), + CentroidDistanceMatcher.instance(), + HausdorffDistanceMatcher.instance(), + SemiHausdorffDistanceMatcher.instance(), + ShapeMatcher.instance() + ); + + public static MatcherRegistry<StringMatcher> STRING_MATCHERS = new MatcherRegistry<> ( + MatchAllStringsMatcher.instance(), + StringEqualityMatcher.instance(), + StringEqualityIgnoreCaseMatcher.instance(), + StringEqualityIgnoreCaseAndAccentMatcher.instance(), + LevenshteinDistanceMatcher.instance(), + DamarauLevenshteinDistanceMatcher.instance() + ); + + private Map<String,T> map = new LinkedHashMap<>(); + + public void register(T matcher) { + //map.put(matcher.toString(), matcher); + map.put(matcher.getClass().getSimpleName(), matcher); + } + + public MatcherRegistry(T... matchers) { + for (T matcher : matchers) register(matcher); + } + + public T get(String name) { + return map.get(name); + } + + public Map<String,T> getMap() { + return map; + } + + public static Matcher getMatcher(MatcherRegistry<? extends Matcher> registry, String name) { + return registry.map.get(name); + } + +} Added: plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/MatchingExtension.java =================================================================== --- plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/MatchingExtension.java (rev 0) +++ plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/MatchingExtension.java 2018-06-17 12:10:51 UTC (rev 5878) @@ -0,0 +1,81 @@ +/* + * (C) 2017 Michaël Michaud + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + * For more information, contact: + * + * m.michael.mich...@orange.fr + */ + +package fr.michaelm.jump.plugin.match; + +import com.vividsolutions.jump.workbench.plugin.Extension; +import com.vividsolutions.jump.workbench.plugin.PlugInContext; + +/** + * Extension containing matching processing also known as join. + * @author Michaël Michaud + * @version 0.8.0 (2018-06-15) + */ +// History +// 0.8.0 (2018-06-15) : refactor to use add/getParameter +// 0.7.5 (2017-03-26) : clean headers and remove dead code before inclusion in +// OpenJUMP PLUS version +// 0.7.4 (2017-03-13) : overlapping method could not handle linear geometries +// 0.7.3 (2014-03-27) : layers created by the plugin are now true layers, not +// "views" on the source layer as views can cause severe +// bugs if schema of the source layer is changed. +// autorise le transfert d'attribut quand il n'y a pas +// d'attribut de type String +// 0.7.2 (2013-07-30) : fix a bug which appears when GeometryMatcher and +// DamarauLevenshteinDistanceMatcher are used simultaneously +// 0.7.1 (2013-04-21) : remember last attribute used if layers did not change +// 0.7.0 (2013-04-07) : add MatchingUpdatePlugIn and MatchEditingPlugIn +// 0.6.2 (2013-03-05) : correction d'une regression empechant tout appariment 1:1 +// 0.6.1 (2013-03-02) : option to compute min distance of matched features +// improve UI labels/I18N for source and target layers +// matchingUpdatePlugIn (work-in-progress, not yet activated) +// 0.6.0 (2013-01-29) : performance improvements +// improve UI labels/I18N for singleSource and singleTarget options +// the threaded process is now interruptable +// 0.5.9 (2012-12-03) : UI labels and I18N +// 0.5.8 (2012-10-07) : fix in 1:N matches wich were only partially found +// 0.5.7 (2012-09- ) : +// 0.5.6 (2012-06-28) : small fix in the UI (attribute aggregation) +// 0.5.5 (2012-05-08) : fix a NPE when geometry+attribute+cardinality was used +// changed the limit definition of damarau-levenshtein +// 0.5.4 (2012-03-12) : add X_MIN_SCORE to reference dataset + fix link layer name +// 0.5.3 (2012-01-17) : recompile to be java 1.5 compatible +// 0.5.2 (2012-01-03) : small fixes in i18n +// 0.5.1 (2011-12-04) : small fix in i18n +// 0.5 (2011-12-01) : initial version +public class MatchingExtension extends Extension { + + public String getName() { + return "Matching Extension (Michaël Michaud)"; + } + + public String getVersion() { + return "0.8.0 (2018-06-15)"; + } + + public void configure(PlugInContext context) throws Exception { + new MatchingPlugIn().initialize(context); + //new MatchingUpdatePlugIn().initialize(context); + new MatchEditingPlugIn().initialize(context); + } + +} \ No newline at end of file Added: plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/MatchingPlugIn.java =================================================================== --- plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/MatchingPlugIn.java (rev 0) +++ plug-ins/MatchingPlugIn/trunk/src/fr/michaelm/jump/plugin/match/MatchingPlugIn.java 2018-06-17 12:10:51 UTC (rev 5878) @@ -0,0 +1,903 @@ +/* + * (C) 2018 Michaël Michaud + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + * For more information, contact: + * + * m.michael.mich...@orange.fr + */ + +package fr.michaelm.jump.plugin.match; + +import com.vividsolutions.jts.geom.Coordinate; +import com.vividsolutions.jts.geom.Geometry; +import com.vividsolutions.jts.geom.GeometryFactory; +import com.vividsolutions.jts.geom.Point; +import com.vividsolutions.jts.operation.distance.DistanceOp; +import com.vividsolutions.jump.task.TaskMonitor; +import com.vividsolutions.jump.feature.AttributeType; +import com.vividsolutions.jump.feature.BasicFeature; +import com.vividsolutions.jump.feature.Feature; +import com.vividsolutions.jump.feature.FeatureCollection; +import com.vividsolutions.jump.feature.FeatureDataset; +import com.vividsolutions.jump.feature.FeatureSchema; +import com.vividsolutions.jump.workbench.Logger; +import com.vividsolutions.jump.workbench.model.Layer; +import com.vividsolutions.jump.workbench.model.StandardCategoryNames; +import com.vividsolutions.jump.workbench.plugin.MultiEnableCheck; +import com.vividsolutions.jump.workbench.plugin.PlugInContext; +import com.vividsolutions.jump.workbench.plugin.ThreadedBasePlugIn; +import com.vividsolutions.jump.workbench.ui.AttributeTypeFilter; +import com.vividsolutions.jump.workbench.ui.GUIUtil; +import com.vividsolutions.jump.workbench.ui.MenuNames; +import com.vividsolutions.jump.workbench.ui.MultiTabInputDialog; +import com.vividsolutions.jump.workbench.ui.renderer.style.BasicStyle; +import com.vividsolutions.jump.workbench.ui.Viewport; +import com.vividsolutions.jump.workbench.ui.renderer.style.RingVertexStyle; + +import fr.michaelm.jump.plugin.match.matcher.*; +import fr.michaelm.util.text.RuleRegistry; +import org.openjump.core.ui.plugin.tools.aggregate.Aggregator; +import org.openjump.core.ui.plugin.tools.aggregate.Aggregators; + +import java.awt.*; +import java.awt.geom.Point2D; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import java.util.Arrays; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JTextField; + +/** + * PlugIn to find features of a source layer matching features of a target layer + * (or reference layer) and optionally transfer attributes between matching + * features. + * @author Michaël Michaud + */ +public class MatchingPlugIn extends ThreadedBasePlugIn { + + private String P_SRC_LAYER = "SourceLayer"; + private String P_SINGLE_SRC = "SingleSource"; + private String P_TGT_LAYER = "TargetLayer"; + private String P_SINGLE_TGT = "SingleTarget"; + private String P_GEOMETRY_MATCHER = "GeometryMatcher"; + private String P_MAX_GEOM_DISTANCE = "MaximumGeometriesDistance"; + private String P_MIN_GEOM_OVERLAP = "MinimumGeometriesOverlap"; + private String P_COPY_MATCHING = "CopyMatchingFeatures"; + private String P_COPY_NOT_MATCHING = "CopyNotMatchingFeatures"; + private String P_DISPLAY_LINKS = "DisplayLinks"; + + private String P_USE_ATTRIBUTES = "UseAttributes"; + private String P_SRC_ATTRIBUTE = "SourceAttribute"; + private String P_SRC_ATTRIBUTE_PREPROCESS = "SourceAttributePreprocess"; + private String P_TGT_ATTRIBUTE = "TargetAttribute"; + private String P_TGT_ATTRIBUTE_PREPROCESS = "TargetAttributePreprocess"; + private String P_ATTRIBUTE_MATCHER = "AttributeMatcher"; + private String P_MAX_STRING_DISTANCE = "MaximumStringDistance"; + private String P_HAS_MAX_STRING_DISTANCE = "HasMaxStringDistance"; + private String P_MIN_STRING_OVERLAP = "MinStringOverlap"; + private String P_HAS_MIN_STRING_OVERLAP = "HasMinStringOverlap"; + + + private String P_TRANSFER_ATTRIBUTES = "TransferAttributes"; + private String P_TRANSFER_BEST_MATCH_ONLY = "TransferBestMatchOnly"; + private String P_STRING_AGGREGATOR = "StringAggregator"; + private String P_INTEGER_AGGREGATOR = "IntegerAggregator"; + private String P_DOUBLE_AGGREGATOR = "DoubleAggregator"; + private String P_DATE_AGGREGATOR = "DateAggregator"; + + + private final String MATCHING = I18NPlug.getI18N("Matching"); + private final String MATCHING_OPTIONS = I18NPlug.getI18N("Matching-options"); + + // Source layer + private final String SOURCE_LAYER = I18NPlug.getI18N("Source-layer"); + private final String SOURCE_LAYER_TOOLTIP = I18NPlug.getI18N("Source-layer-tooltip"); + private final String SINGLE_SOURCE = I18NPlug.getI18N("Single-source"); + private final String SINGLE_SOURCE_TOOLTIP = I18NPlug.getI18N("Single-source-tooltip"); + + // Target layer + private final String TARGET_LAYER = I18NPlug.getI18N("Target-layer"); + private final String TARGET_LAYER_TOOLTIP = I18NPlug.getI18N("Target-layer-tooltip"); + private final String SINGLE_TARGET = I18NPlug.getI18N("Single-target"); + private final String SINGLE_TARGET_TOOLTIP = I18NPlug.getI18N("Single-target-tooltip"); + + // Geometry matcher + private final String GEOMETRIC_OPTIONS = I18NPlug.getI18N("Geometric-options"); + private final String GEOMETRY_MATCHER = I18NPlug.getI18N("Geometry-matcher"); + private final String MAXIMUM_DISTANCE = I18NPlug.getI18N("Maximum-distance"); + private final String MINIMUM_OVERLAPPING = I18NPlug.getI18N("Minimum-overlapping"); + + // Output options + private final String OUTPUT_OPTIONS = I18NPlug.getI18N("Output-options"); + private final String COPY_MATCHING_FEATURES = I18NPlug.getI18N("Copy-matching-features"); + private final String COPY_NOT_MATCHING_FEATURES = I18NPlug.getI18N("Copy-not-matching-features"); + private final String DISPLAY_LINKS = I18NPlug.getI18N("Display-links"); + + // Attributes options + private final String ATTRIBUTE_OPTIONS = I18NPlug.getI18N("Attribute-options"); + private final String USE_ATTRIBUTES = I18NPlug.getI18N("Use-attributes"); + private final String SOURCE_LAYER_ATTRIBUTE = I18NPlug.getI18N("Source-layer-attribute"); + private final String SOURCE_ATT_PREPROCESSING = I18NPlug.getI18N("Source-att-preprocessing"); + private final String TARGET_LAYER_ATTRIBUTE = I18NPlug.getI18N("Target-layer-attribute"); + private final String TARGET_ATT_PREPROCESSING = I18NPlug.getI18N("Target-att-preprocessing"); + private final String ATTRIBUTE_MATCHER = I18NPlug.getI18N("Attribute-matcher"); + private final String MAXIMUM_STRING_DISTANCE = I18NPlug.getI18N("Maximum-string-distance"); + private final String MINIMUM_STRING_OVERLAPPING = I18NPlug.getI18N("Minimum-string-overlapping"); + + // Attribute transfer / aggregation + private final String TRANSFER_OPTIONS = I18NPlug.getI18N("Transfer-options"); + private final String TRANSFER_TO_REFERENCE_LAYER = I18NPlug.getI18N("Transfer-to-reference-layer"); + private final String TRANSFER_BEST_MATCH_ONLY = I18NPlug.getI18N("Transfer-best-match-only"); + + private final String STRING_AGGREGATION = I18NPlug.getI18N("String-aggregation"); + private final String INTEGER_AGGREGATION = I18NPlug.getI18N("Integer-aggregation"); + private final String DOUBLE_AGGREGATION = I18NPlug.getI18N("Double-aggregation"); + private final String DATE_AGGREGATION = I18NPlug.getI18N("Date-aggregation"); + + // Processing and Error messages + private final String SEARCHING_MATCHES = I18NPlug.getI18N("Searching-matches"); + private final String MISSING_INPUT_LAYER = I18NPlug.getI18N("Missing-input-layer"); + private final String CHOOSE_MATCHER = I18NPlug.getI18N("Choose-geometry-or-attribute-matcher"); + + // Parameters : source layer and cardinality + private String source_layer_name; + private boolean single_source = false; + // Parameters : target layer and cardinality + private String target_layer_name; + private boolean single_target = false; + + // Parameters : geometry parameters + private GeometryMatcher geometry_matcher = CentroidDistanceMatcher.instance(); + private double max_distance = geometry_matcher.getMaximumDistance(); + private boolean set_max_distance = !Double.isNaN(max_distance); + private double min_overlapping = geometry_matcher.getMinimumOverlapping(); + private boolean set_min_overlapping = !Double.isNaN(min_overlapping); + + // Parameters : output options + private boolean copy_matching_features = true; + private boolean copy_not_matching_features; + private boolean display_links = false; + + // Parameters : attribute parameters + private boolean use_attributes = false; + private String source_att_preprocessing = ""; + private String source_layer_attribute; + private String target_att_preprocessing = ""; + private String target_layer_attribute; + private StringMatcher attribute_matcher = + StringEqualityIgnoreCaseAndAccentMatcher.instance(); + private double max_string_distance = attribute_matcher.getMaximumDistance(); + private boolean has_max_string_distance = !Double.isNaN(max_string_distance); + private double min_string_overlapping = attribute_matcher.getMinimumOverlapping(); + private boolean has_min_string_overlapping = !Double.isNaN(min_string_overlapping); + + // Parameters : transfer and aggregation + private boolean transfer = true; + private boolean transfer_best_match_only = false; + // Ignore null by default + //private boolean ignore_null = true; + private Aggregator string_aggregator = + Aggregators.getAggregator(new Aggregators.ConcatenateUnique(true).getName()); + private Aggregator integer_aggregator = + Aggregators.getAggregator(new Aggregators.IntSum().getName()); + private Aggregator double_aggregator = + Aggregators.getAggregator(new Aggregators.DoubleMean(true).getName()); + private Aggregator date_aggregator = + Aggregators.getAggregator(new Aggregators.DateMean(true).getName()); + + // initialisation of parameters + { + addParameter(P_SRC_LAYER, source_layer_name); + addParameter(P_SINGLE_SRC, single_source); + addParameter(P_TGT_LAYER, target_layer_name); + addParameter(P_SINGLE_TGT, single_target); + addParameter(P_GEOMETRY_MATCHER, geometry_matcher.getClass().getSimpleName()); + addParameter(P_MAX_GEOM_DISTANCE, max_distance); + addParameter(P_MIN_GEOM_OVERLAP, min_overlapping); + addParameter(P_COPY_MATCHING, copy_matching_features); + addParameter(P_COPY_NOT_MATCHING, copy_not_matching_features); + addParameter(P_DISPLAY_LINKS, display_links); + + addParameter(P_USE_ATTRIBUTES, use_attributes); + addParameter(P_SRC_ATTRIBUTE, source_layer_attribute); + addParameter(P_SRC_ATTRIBUTE_PREPROCESS, source_att_preprocessing); + addParameter(P_TGT_ATTRIBUTE, target_layer_attribute); + addParameter(P_TGT_ATTRIBUTE_PREPROCESS, target_att_preprocessing); + addParameter(P_ATTRIBUTE_MATCHER, attribute_matcher.getClass().getSimpleName()); + addParameter(P_MAX_STRING_DISTANCE, max_string_distance); + addParameter(P_HAS_MAX_STRING_DISTANCE, has_max_string_distance); + addParameter(P_MIN_STRING_OVERLAP, min_string_overlapping); + addParameter(P_HAS_MIN_STRING_OVERLAP, has_min_string_overlapping); + + addParameter(P_TRANSFER_ATTRIBUTES, transfer); + addParameter(P_TRANSFER_BEST_MATCH_ONLY, transfer_best_match_only); + addParameter(P_STRING_AGGREGATOR, string_aggregator.getName()); + addParameter(P_INTEGER_AGGREGATOR, integer_aggregator.getName()); + addParameter(P_DOUBLE_AGGREGATOR, double_aggregator.getName()); + addParameter(P_DATE_AGGREGATOR, date_aggregator.getName()); + } + + public MatchingPlugIn() { + } + + public String getName() { + return MATCHING; + } + + public void initialize(PlugInContext context) throws Exception { + + context.getFeatureInstaller().addMainMenuPlugin( + this, new String[]{MenuNames.PLUGINS, MATCHING}, + MATCHING + "...", + false, null, new MultiEnableCheck() + .add(context.getCheckFactory().createTaskWindowMustBeActiveCheck()) + .add(context.getCheckFactory().createAtLeastNLayersMustExistCheck(1))); + } + + /** + * Execute method initialize the plugin interface and get all the + * parameters from the user. + */ + public boolean execute(PlugInContext context) throws Exception { + + try { + RuleRegistry.loadRules( + context.getWorkbenchContext().getWorkbench().getPlugInManager().getPlugInDirectory().getPath() + "\\Rules" + ); + } catch (IllegalArgumentException iae) { + Logger.warn(iae.getMessage()); + context.getWorkbenchFrame().warnUser(I18NPlug.getMessage("Missing-directory", + new String[]{ + context.getWorkbenchContext().getWorkbench() + .getPlugInManager().getPlugInDirectory().getName() + + "/Rules" + } + )); + } + + //////////////////////////////////////////////////////////////////////// + // UI : CREATE MULTITAB INPUT DIALOG + //////////////////////////////////////////////////////////////////////// + + final MultiTabInputDialog dialog = new MultiTabInputDialog( + context.getWorkbenchFrame(), MATCHING_OPTIONS, GEOMETRIC_OPTIONS, true); + initDialog(dialog, context); + + GUIUtil.centreOnWindow(dialog); + dialog.setVisible(true); + + if (dialog.wasOKPressed()) { + + // Get source layer parameters + Layer source_layer = dialog.getLayer(SOURCE_LAYER); + source_layer_name = source_layer.getName(); + single_source = dialog.getBoolean(SINGLE_SOURCE); + + // Get target layer parameters + Layer target_layer = dialog.getLayer(TARGET_LAYER); + target_layer_name = target_layer.getName(); + single_target = dialog.getBoolean(SINGLE_TARGET); + + // Get geometry matcher and set its parameters + geometry_matcher = (GeometryMatcher)dialog.getValue(GEOMETRY_MATCHER); + max_distance = dialog.getDouble(MAXIMUM_DISTANCE); + min_overlapping = dialog.getDouble(MINIMUM_OVERLAPPING); + geometry_matcher.setMaximumDistance(max_distance); + geometry_matcher.setMinimumOverlapping(min_overlapping); + + // Get output options + copy_matching_features = dialog.getBoolean(COPY_MATCHING_FEATURES); + copy_not_matching_features = dialog.getBoolean(COPY_NOT_MATCHING_FEATURES); + display_links = dialog.getBoolean(DISPLAY_LINKS); + + // get attribute options + use_attributes = dialog.getBoolean(USE_ATTRIBUTES); + source_layer_attribute = dialog.getText(SOURCE_LAYER_ATTRIBUTE); + source_att_preprocessing = dialog.getText(SOURCE_ATT_PREPROCESSING); + target_layer_attribute = dialog.getText(TARGET_LAYER_ATTRIBUTE); + target_att_preprocessing = dialog.getText(TARGET_ATT_PREPROCESSING); + attribute_matcher = (StringMatcher)dialog.getValue(ATTRIBUTE_MATCHER); + max_string_distance = dialog.getDouble(MAXIMUM_STRING_DISTANCE); + min_string_overlapping = dialog.getDouble(MINIMUM_STRING_OVERLAPPING); + if (!use_attributes) attribute_matcher = MatchAllStringsMatcher.MATCH_ALL; + else attribute_matcher.setAttributes(source_layer_attribute, + target_layer_attribute); + attribute_matcher.setMaximumDistance(max_string_distance); + attribute_matcher.setMinimumOverlapping(min_string_overlapping); + attribute_matcher.setSourceRule(RuleRegistry.getRule(source_att_preprocessing)); + attribute_matcher.setTargetRule(RuleRegistry.getRule(target_att_preprocessing)); + + // get transfer options + transfer = dialog.getBoolean(TRANSFER_TO_REFERENCE_LAYER); + transfer_best_match_only = dialog.getBoolean(TRANSFER_BEST_MATCH_ONLY); + string_aggregator = (Aggregator)dialog.getComboBox(STRING_AGGREGATION).getSelectedItem(); + integer_aggregator = (Aggregator)dialog.getComboBox(INTEGER_AGGREGATION).getSelectedItem(); + double_aggregator = (Aggregator)dialog.getComboBox(DOUBLE_AGGREGATION).getSelectedItem(); + date_aggregator = (Aggregator)dialog.getComboBox(DATE_AGGREGATION).getSelectedItem(); + + System.out.println("start adding parameters"); + + addParameter(P_SRC_LAYER, source_layer_name); + addParameter(P_SINGLE_SRC, single_source); + addParameter(P_TGT_LAYER, target_layer_name); + addParameter(P_SINGLE_TGT, single_target); + addParameter(P_GEOMETRY_MATCHER, geometry_matcher.getClass().getSimpleName()); + addParameter(P_MAX_GEOM_DISTANCE, max_distance); + addParameter(P_MIN_GEOM_OVERLAP, min_overlapping); + addParameter(P_COPY_MATCHING, copy_matching_features); + addParameter(P_COPY_NOT_MATCHING, copy_not_matching_features); + addParameter(P_DISPLAY_LINKS, display_links); + + addParameter(P_USE_ATTRIBUTES, use_attributes); + addParameter(P_SRC_ATTRIBUTE, source_layer_attribute); + addParameter(P_SRC_ATTRIBUTE_PREPROCESS, source_att_preprocessing); + addParameter(P_TGT_ATTRIBUTE, target_layer_attribute); + addParameter(P_TGT_ATTRIBUTE_PREPROCESS, target_att_preprocessing); + addParameter(P_ATTRIBUTE_MATCHER, attribute_matcher.getClass().getSimpleName()); + addParameter(P_MAX_STRING_DISTANCE, max_string_distance); + addParameter(P_HAS_MAX_STRING_DISTANCE, has_max_string_distance); + addParameter(P_MIN_STRING_OVERLAP, min_string_overlapping); + addParameter(P_HAS_MIN_STRING_OVERLAP, has_min_string_overlapping); + + addParameter(P_TRANSFER_ATTRIBUTES, transfer); + addParameter(P_TRANSFER_BEST_MATCH_ONLY, transfer_best_match_only); + addParameter(P_STRING_AGGREGATOR, string_aggregator.getName()); + addParameter(P_INTEGER_AGGREGATOR, integer_aggregator.getName()); + addParameter(P_DOUBLE_AGGREGATOR, double_aggregator.getName()); + addParameter(P_DATE_AGGREGATOR, date_aggregator.getName()); + + if ((geometry_matcher instanceof MatchAllMatcher) && !use_attributes) { + context.getWorkbenchFrame().warnUser(CHOOSE_MATCHER); + return false; + } + return true; + } + else return false; + + } + + private void initDialog(final MultiTabInputDialog dialog, final PlugInContext context) { + + //////////////////////////////////////////////////////////////////////// + // UI : INITIALIZE LAYERS FROM LAST ONES OR FROM CONTEXT + //////////////////////////////////////////////////////////////////////// + + Layer source_layer; + Layer target_layer; + source_layer = context.getLayerManager().getLayer(source_layer_name); + if (source_layer == null) source_layer = context.getCandidateLayer(0); + + target_layer = context.getLayerManager().getLayer(target_layer_name); + int layerNumber = context.getLayerManager().getLayers().size(); + if (target_layer == null) target_layer = context.getCandidateLayer(layerNumber>1?1:0); + + //////////////////////////////////////////////////////////////////////// + // UI : CHOOSE SOURCE LAYER AND SOURCE CARDINALITY + //////////////////////////////////////////////////////////////////////// + + dialog.addLabel("<html><b>"+GEOMETRIC_OPTIONS+"</b></html>"); + + final JComboBox jcb_layer = dialog.addLayerComboBox(SOURCE_LAYER, + source_layer, SOURCE_LAYER_TOOLTIP, context.getLayerManager()); + jcb_layer.setPreferredSize(new Dimension(220,20)); + jcb_layer.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) {updateDialog(dialog);} + }); + final JCheckBox singleSourceFeatureCheckBox = dialog.addCheckBox( + SINGLE_SOURCE, single_source, SINGLE_SOURCE_TOOLTIP); + + //////////////////////////////////////////////////////////////////////// + // UI : CHOOSE GEOMETRY MATCHER + //////////////////////////////////////////////////////////////////////// + Collection<GeometryMatcher> geomMatcherList = + MatcherRegistry.GEOMETRY_MATCHERS.getMap().values(); + final JComboBox<GeometryMatcher> jcb_geom_operation = + dialog.addComboBox(GEOMETRY_MATCHER, geometry_matcher, geomMatcherList, null); + + final JTextField jtf_dist = dialog.addDoubleField(MAXIMUM_DISTANCE, max_distance, 12, null); + jtf_dist.setEnabled(set_max_distance); + + final JTextField jtf_overlap = dialog.addDoubleField(MINIMUM_OVERLAPPING, min_overlapping, 12, null); + jtf_overlap.setEnabled(set_min_overlapping); + + //////////////////////////////////////////////////////////////////////// + // UI : CHOOSE TARGET LAYER AND SOURCE CARDINALITY + //////////////////////////////////////////////////////////////////////// + + final JComboBox jcb_layer_tgt = dialog.addLayerComboBox(TARGET_LAYER, + target_layer, TARGET_LAYER_TOOLTIP, context.getLayerManager()); + jcb_layer_tgt.setPreferredSize(new Dimension(220,20)); + jcb_layer_tgt.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) {updateDialog(dialog);} + }); + final JCheckBox singleTargetFeatureCheckBox = dialog.addCheckBox( + SINGLE_TARGET, single_target, SINGLE_TARGET_TOOLTIP); + + jcb_geom_operation.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + updateDialog(dialog); + geometry_matcher = (GeometryMatcher)jcb_geom_operation.getSelectedItem(); + jtf_dist.setText(""+geometry_matcher.getMaximumDistance()); + jtf_overlap.setText(""+geometry_matcher.getMinimumOverlapping()); + } + }); + + //////////////////////////////////////////////////////////////////////// + // UI : CHOOSE OUTPUT OPTIONS + //////////////////////////////////////////////////////////////////////// + dialog.addSeparator(); + dialog.addLabel("<html><b>"+OUTPUT_OPTIONS+"</b></html>"); + + final JCheckBox jcb_new_layer_match = dialog.addCheckBox(COPY_MATCHING_FEATURES, copy_matching_features, null); + final JCheckBox jcb_new_layer_diff = dialog.addCheckBox(COPY_NOT_MATCHING_FEATURES, copy_not_matching_features, null); + final JCheckBox jcb_display_links = dialog.addCheckBox(DISPLAY_LINKS, display_links, null); + + //////////////////////////////////////////////////////////////////////// + // UI : CHOOSE ATTRIBUTE OPTIONS + //////////////////////////////////////////////////////////////////////// + dialog.addPane(ATTRIBUTE_OPTIONS); + + final JCheckBox jcb_use_attributes = dialog.addCheckBox(USE_ATTRIBUTES, use_attributes, null); + + final JComboBox jcb_src_att_preprocessing = dialog.addComboBox(SOURCE_ATT_PREPROCESSING, + source_att_preprocessing, Arrays.asList(RuleRegistry.getRules()), null); + final JComboBox jcb_src_attribute = dialog.addAttributeComboBox( + SOURCE_LAYER_ATTRIBUTE, SOURCE_LAYER, AttributeTypeFilter.STRING_FILTER, null); + jcb_src_attribute.setSelectedItem(source_layer_attribute); + jcb_src_attribute.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + if (jcb_src_attribute.getSelectedItem() != null) { + source_layer_attribute = jcb_src_attribute.getSelectedItem().toString(); + } + } + }); + + + // Initialize string matching options + Collection<StringMatcher> stringMatcherList = + MatcherRegistry.STRING_MATCHERS.getMap().values(); + final JComboBox<StringMatcher> jcb_attr_operation = dialog.addComboBox( + ATTRIBUTE_MATCHER, attribute_matcher, stringMatcherList, null); + + final JTextField jtf_string_dist = dialog.addDoubleField(MAXIMUM_STRING_DISTANCE, max_string_distance, 12, null); + jtf_string_dist.setEnabled(has_max_string_distance); + + final JTextField jtf_string_overlap = dialog.addDoubleField(MINIMUM_STRING_OVERLAPPING, min_string_overlapping, 12, null); + jtf_string_overlap.setEnabled(has_min_string_overlapping); + + final JComboBox jcb_tgt_att_preprocessing = dialog.addComboBox(TARGET_ATT_PREPROCESSING, + target_att_preprocessing, Arrays.asList(RuleRegistry.getRules()), null); + final JComboBox jcb_tgt_attribute = dialog.addAttributeComboBox( + TARGET_LAYER_ATTRIBUTE, TARGET_LAYER, AttributeTypeFilter.STRING_FILTER, null); + jcb_tgt_attribute.setSelectedItem(target_layer_attribute); + jcb_tgt_attribute.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + if (jcb_tgt_attribute.getSelectedItem() != null) { + target_layer_attribute = jcb_tgt_attribute.getSelectedItem().toString(); + } + } + }); + + jcb_attr_operation.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) {updateDialog(dialog);} + }); + jcb_use_attributes.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) {updateDialog(dialog);} + }); + + //////////////////////////////////////////////////////////////////////// + // UI : TRANSFER ATTRIBUTE / AGGREGATION + //////////////////////////////////////////////////////////////////////// + dialog.addSeparator(); + dialog.addPane(TRANSFER_OPTIONS); + dialog.addSubTitle(TRANSFER_OPTIONS); + + final JCheckBox jcb_transfer = dialog.addCheckBox(TRANSFER_TO_REFERENCE_LAYER, transfer, null); + jcb_transfer.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) {updateDialog(dialog);} + }); + final JCheckBox jcb_transfer_best_match_only = dialog.addCheckBox(TRANSFER_BEST_MATCH_ONLY, transfer_best_match_only, null); + jcb_transfer_best_match_only.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) {updateDialog(dialog);} + }); + + final JComboBox jcb_string_aggregator = dialog.addComboBox( + STRING_AGGREGATION, string_aggregator, + Aggregators.getAggregators(AttributeType.STRING).values(), null + ); + final JComboBox jcb_integer_aggregator = dialog.addComboBox( + INTEGER_AGGREGATION, integer_aggregator, + Aggregators.getAggregators(AttributeType.INTEGER).values(), null + ); + final JComboBox jcb_double_aggregator = dialog.addComboBox( + DOUBLE_AGGREGATION, double_aggregator, + Aggregators.getAggregators(AttributeType.DOUBLE).values(), null + ); + final JComboBox jcb_date_aggregator = dialog.addComboBox( + DATE_AGGREGATION, date_aggregator, + Aggregators.getAggregators(AttributeType.DATE).values(), null + ); + + updateDialog(dialog); + } + + + // update dialog is called by several component listeners to update the + // dialog before the final validation + // 2012-06-29 : component values are stored in local variables as this is + // not necessary to change the plugin parameters until final validation + private void updateDialog(MultiTabInputDialog dialog) { + + // Updates related to a geometry_matcher change + geometry_matcher = (GeometryMatcher)dialog.getValue(GEOMETRY_MATCHER); + geometry_matcher.setMaximumDistance(dialog.getDouble(MAXIMUM_DISTANCE)); + geometry_matcher.setMinimumOverlapping(dialog.getDouble(MINIMUM_OVERLAPPING)); + dialog.setFieldEnabled(MAXIMUM_DISTANCE, !Double.isNaN(geometry_matcher.getMaximumDistance())); + dialog.setFieldEnabled(MINIMUM_OVERLAPPING, !Double.isNaN(geometry_matcher.getMinimumOverlapping())); + //String sMatcher = dialog.getText(GEOMETRY_MATCHER); + //Matcher _matcher = MatcherRegistry.GEOMETRY_MATCHERS.get(sMatcher); + //double dmax = _matcher.getMaximumDistance(); + //double omin = _matcher.getMinimumOverlapping(); + //boolean _set_max_distance = !Double.isNaN(dmax); + //boolean _set_min_overlapping = !Double.isNaN(omin); + //dialog.setFieldEnabled(MAXIMUM_DISTANCE, _set_max_distance); + //dialog.setFieldEnabled(MINIMUM_OVERLAPPING, _set_min_overlapping); + + + // Updates related to a layer change + Layer srcLayer = dialog.getLayer(SOURCE_LAYER); + Layer tgtLayer = dialog.getLayer(TARGET_LAYER); + boolean srcLayer_has_attributes = + srcLayer.getFeatureCollectionWrapper().getFeatureSchema().getAttributeCount() > 1; + boolean srcLayer_has_string_attributes = + AttributeTypeFilter.STRING_FILTER.filter(srcLayer.getFeatureCollectionWrapper().getFeatureSchema()).size() > 0; + boolean tgtLayer_has_string_attributes = + AttributeTypeFilter.STRING_FILTER.filter(tgtLayer.getFeatureCollectionWrapper().getFeatureSchema()).size() > 0; + dialog.setFieldEnabled(USE_ATTRIBUTES, srcLayer_has_string_attributes && + tgtLayer_has_string_attributes); + dialog.setTabEnabled(ATTRIBUTE_OPTIONS, srcLayer_has_string_attributes && + tgtLayer_has_string_attributes); + if (!srcLayer_has_string_attributes || !tgtLayer_has_string_attributes) { + attribute_matcher = MatchAllStringsMatcher.MATCH_ALL; + //dialog.getCheckBox(USE_ATTRIBUTES).setSelected(false); + } else { + attribute_matcher = (StringMatcher)dialog.getValue(ATTRIBUTE_MATCHER); + } + //dialog.getComboBox(SOURCE_LAYER_ATTRIBUTE).setSelectedItem(source_layer_attribute); + //dialog.getComboBox(TARGET_LAYER_ATTRIBUTE).setSelectedItem(target_layer_attribute); + + // Updates related to attribute transfer + dialog.setTabEnabled(TRANSFER_OPTIONS, srcLayer_has_attributes); + + boolean _transfer = dialog.getBoolean(TRANSFER_TO_REFERENCE_LAYER); + dialog.setFieldEnabled(TRANSFER_BEST_MATCH_ONLY, _transfer); + + boolean _transfer_best_match_only = dialog.getBoolean(TRANSFER_BEST_MATCH_ONLY); + dialog.setFieldEnabled(STRING_AGGREGATION, _transfer && !_transfer_best_match_only); + dialog.setFieldEnabled(INTEGER_AGGREGATION, _transfer && !_transfer_best_match_only); + dialog.setFieldEnabled(DOUBLE_AGGREGATION, _transfer && !_transfer_best_match_only); + dialog.setFieldEnabled(DATE_AGGREGATION, _transfer && !_transfer_best_match_only); + + // Updates related to attribute matching + boolean _use_attributes = dialog.getBoolean(USE_ATTRIBUTES); + dialog.setFieldEnabled(SOURCE_LAYER_ATTRIBUTE, _use_attributes); + dialog.setFieldEnabled(SOURCE_ATT_PREPROCESSING, _use_attributes); + dialog.setFieldEnabled(TARGET_LAYER_ATTRIBUTE, _use_attributes); + dialog.setFieldEnabled(TARGET_ATT_PREPROCESSING, _use_attributes); + + dialog.setFieldEnabled(ATTRIBUTE_MATCHER, _use_attributes); + //attribute_matcher = (StringMatcher)dialog.getValue(ATTRIBUTE_MATCHER); + //String aMatcher = dialog.getText(ATTRIBUTE_MATCHER); + //StringMatcher _attribute_matcher = (StringMatcher)MatcherRegistry.STRING_MATCHERS.get(aMatcher); + + dialog.setFieldEnabled(MAXIMUM_STRING_DISTANCE, _use_attributes && + (attribute_matcher instanceof LevenshteinDistanceMatcher || + attribute_matcher instanceof DamarauLevenshteinDistanceMatcher)); + + } + + /** + * Run executes the main process, looping through matching layer, and + * looking for candidates in the reference layer. + */ + public void run(TaskMonitor monitor, PlugInContext context) throws Exception { + + // layers and cardinality constraints + source_layer_name = getStringParam(P_SRC_LAYER); + single_source = getBooleanParam(P_SINGLE_SRC); + target_layer_name = getStringParam(P_TGT_LAYER); + single_target = getBooleanParam(P_SINGLE_TGT); + + // geometry matcher + geometry_matcher = MatcherRegistry.GEOMETRY_MATCHERS + .get(getStringParam(P_GEOMETRY_MATCHER)); + if (geometry_matcher == null) { + throw new Exception("GeometryMatcher '" + getStringParam(P_GEOMETRY_MATCHER) + "' has not been found"); + } + max_distance = getDoubleParam(P_MAX_GEOM_DISTANCE); + min_overlapping = getDoubleParam(P_MIN_GEOM_OVERLAP); + copy_matching_features = getBooleanParam(P_COPY_MATCHING); + copy_not_matching_features = getBooleanParam(P_COPY_NOT_MATCHING); + display_links = getBooleanParam(P_DISPLAY_LINKS); + // derived + geometry_matcher.setMaximumDistance(max_distance); + geometry_matcher.setMinimumOverlapping(min_overlapping); + + // attribute matcher + use_attributes = getBooleanParam(P_USE_ATTRIBUTES); + source_layer_attribute = getStringParam(P_SRC_ATTRIBUTE); + source_att_preprocessing = getStringParam(P_SRC_ATTRIBUTE_PREPROCESS); + target_layer_attribute = getStringParam(P_TGT_ATTRIBUTE); + target_att_preprocessing = getStringParam(P_TGT_ATTRIBUTE_PREPROCESS); + attribute_matcher = MatcherRegistry.STRING_MATCHERS + .get(getStringParam(P_ATTRIBUTE_MATCHER)); + max_string_distance = getDoubleParam(P_MAX_STRING_DISTANCE); + has_max_string_distance = getBooleanParam(P_HAS_MAX_STRING_DISTANCE); + min_string_overlapping = getDoubleParam(P_MIN_STRING_OVERLAP); + has_min_string_overlapping = getBooleanParam(P_HAS_MIN_STRING_OVERLAP); + // derived + if (!use_attributes) attribute_matcher = MatchAllStringsMatcher.MATCH_ALL; + else { + if (attribute_matcher == null) { + throw new Exception("Attribute Matcher '" + getStringParam(P_ATTRIBUTE_MATCHER) + "' has not been found"); + } + attribute_matcher.setAttributes(source_layer_attribute, target_layer_attribute); + attribute_matcher.setMaximumDistance(max_string_distance); + attribute_matcher.setMinimumOverlapping(min_string_overlapping); + } + + // transfer options + transfer = getBooleanParam(P_TRANSFER_ATTRIBUTES); + transfer_best_match_only = getBooleanParam(P_TRANSFER_BEST_MATCH_ONLY); + string_aggregator = Aggregators.getAggregator(getStringParam(P_STRING_AGGREGATOR)); + string_aggregator.setIgnoreNull(true); + integer_aggregator = Aggregators.getAggregator(getStringParam(P_INTEGER_AGGREGATOR)); + integer_aggregator.setIgnoreNull(true); + double_aggregator = Aggregators.getAggregator(getStringParam(P_DOUBLE_AGGREGATOR)); + double_aggregator.setIgnoreNull(true); + date_aggregator = Aggregators.getAggregator(getStringParam(P_DATE_AGGREGATOR)); + date_aggregator.setIgnoreNull(true); + + + Layer source_layer = context.getLayerManager().getLayer(source_layer_name); @@ Diff output truncated at 100000 characters. @@ ------------------------------------------------------------------------------ Check out the vibrant tech community on one of the world's most engaging tech sites, Slashdot.org! http://sdm.link/slashdot _______________________________________________ Jump-pilot-devel mailing list Jump-pilot-devel@lists.sourceforge.net https://lists.sourceforge.net/lists/listinfo/jump-pilot-devel