/* * Copyright (C) 2013. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 or * version 2 as published by the Free Software Foundation. * * 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. */ package uk.me.parabola.mkgmap.osmstyle; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import uk.me.parabola.imgfmt.Utils; import uk.me.parabola.imgfmt.app.Coord; import uk.me.parabola.imgfmt.app.net.AccessTagsAndBits; import uk.me.parabola.log.Logger; import uk.me.parabola.mkgmap.reader.osm.Element; import uk.me.parabola.mkgmap.reader.osm.GType; import uk.me.parabola.mkgmap.reader.osm.Node; import uk.me.parabola.mkgmap.reader.osm.Relation; import uk.me.parabola.mkgmap.reader.osm.RestrictionRelation; import uk.me.parabola.mkgmap.reader.osm.Way; import uk.me.parabola.util.MultiIdentityHashMap; /** * Merges connected roads with identical road relevant tags based on the OSM elements * and the GType class. * * @author WanMil */ public class RoadMerger { private static final Logger log = Logger.getLogger(RoadMerger.class); private static final double MAX_MERGE_ANGLE = 130d; /** maps which coord of a way(id) are restricted - they should not be merged */ private final MultiIdentityHashMap<Coord, Long> restrictions = new MultiIdentityHashMap<>(); /** maps the start point of a road to its road definition */ private final MultiIdentityHashMap<Coord, ConvertedWay> startPoints = new MultiIdentityHashMap<>(); /** maps the end point of a road to its road definition */ private final MultiIdentityHashMap<Coord, ConvertedWay> endPoints = new MultiIdentityHashMap<>(); /** * For these tags two ways need to have an equal value so that their roads can be merged. */ private final static Set<String> mergeTagsEqualValue = new HashSet<String>() { { add("mkgmap:label:1"); add("mkgmap:label:2"); add("mkgmap:label:3"); add("mkgmap:label:4"); add("mkgmap:postal_code"); add("mkgmap:city"); add("mkgmap:region"); add("mkgmap:country"); add("mkgmap:is_in"); add("mkgmap:skipSizeFilter"); add("mkgmap:synthesised"); add("mkgmap:highest-resolution-only"); add("mkgmap:flare-check"); add("mkgmap:numbers"); } }; /** * Checks if two strings are equal ({@code null} supported). * @param s1 first string ({@code null} allowed) * @param s2 second string ({@code null} allowed) * @return {@code true} both strings are equal or both {@code null}; {@code false} both strings are not equal */ private static boolean stringEquals(String s1, String s2) { if (s1 == null) return s2 == null; return s1.equals(s2); } /** * We must not merge roads at via points of restriction relations * if the way is referenced in the restriction. * @param restrictionRels */ private void workoutRestrictionRelations(List<RestrictionRelation> restrictionRels) { for (RestrictionRelation rel : restrictionRels) { Set<Long> restrictionWayIds = rel.getWayIds(); for (Coord via: rel.getViaCoords()){ HashSet<ConvertedWay> roadAtVia = new HashSet<>(); roadAtVia.addAll(startPoints.get(via)); roadAtVia.addAll(endPoints.get(via)); for (ConvertedWay r: roadAtVia){ long wayId = r.getWay().getId(); if (restrictionWayIds.contains(wayId)) restrictions.add(via, wayId); } } } } private void workoutThroughRoutes(List<Relation> throughRouteRelations) { for (Relation relation : throughRouteRelations) { Node node = null; Way w1 = null; Way w2 = null; for (Map.Entry<String, Element> member : relation.getElements()) { if (member.getValue() instanceof Node) { if (node == null) node = (Node) member.getValue(); else log.warn("Through route relation " + relation.toBrowseURL() + " has more than 1 node"); } else if (member.getValue() instanceof Way) { Way w = (Way) member.getValue(); if (w1 == null) w1 = w; else if (w2 == null) w2 = w; else log.warn("Through route relation " + relation.toBrowseURL() + " has more than 2 ways"); } } if (node == null) log.warn("Through route relation " + relation.toBrowseURL() + " is missing the junction node"); if (w1 == null || w2 == null) log.warn("Through route relation " + relation.toBrowseURL() + " should reference 2 ways that meet at the junction node"); if (node != null && w1 != null && w2 != null) { restrictions.add(node.getLocation(), w1.getId()); restrictions.add(node.getLocation(), w2.getId()); } } } private boolean hasRestriction(Coord c, Way w) { if (w.isViaWay()) return true; List<Long> wayRestrictions = restrictions.get(c); return wayRestrictions.contains(w.getId()); } /** * Merges {@code road2} into {@code road1}. This means that * only the way id and the tags of {@code road1} is kept. * For the tag it should not matter because all tags used after the * RoadMerger are compared to be the same. * * @param road1 first road (will keep the merged road) * @param road2 second road */ private void mergeRoads(ConvertedWay road1, ConvertedWay road2) { // Removes the second line, // Merges the points in the first one List<Coord> points1 = road1.getWay().getPoints(); List<Coord> points2 = road2.getWay().getPoints(); Coord mergePoint = points2.get(0); Coord endPoint= points2.get(points2.size()-1); startPoints.removeMapping(mergePoint, road2); endPoints.removeMapping(endPoint, road2); endPoints.removeMapping(mergePoint, road1); points1.addAll(points2.subList(1, points2.size())); endPoints.add(endPoint, road1); // merge the POI info String wayPOI2 = road2.getWay().getTag(StyledConverter.WAY_POI_NODE_IDS); if (wayPOI2 != null){ String WayPOI1 = road1.getWay().getTag(StyledConverter.WAY_POI_NODE_IDS); if (wayPOI2.equals(WayPOI1) == false){ if (WayPOI1 == null) WayPOI1 = ""; // store combination of both ways. This might contain // duplicates, but that is not a problem. road1.getWay().addTag(StyledConverter.WAY_POI_NODE_IDS, WayPOI1 + wayPOI2); } } // // the mergePoint is now used by one highway less mergePoint.decHighwayCount(); // road2 is removed - it must not be part of a restriction assert (restrictions.get(endPoint).contains(road2.getWay().getId()) == false); } /** * Merge the roads and copy the results to the given lists. * @param resultingWays list for the merged (and not mergeable) ways * @param resultingGTypes list for the merged (and not mergeable) GTypes */ public List<ConvertedWay> merge(List<ConvertedWay> convertedWays, List<RestrictionRelation> restrictions, List<Relation> throughRouteRelations) { List<ConvertedWay> result = new ArrayList<>(); List<ConvertedWay> roadsToMerge = new ArrayList<>(convertedWays.size()); for (int i = 0; i < convertedWays.size(); i++) { ConvertedWay cw = convertedWays.get(i); if (cw.isValid() && cw.isRoad()) roadsToMerge.add(cw); } int noRoadsBeforeMerge = roadsToMerge.size(); int noMerges = 0; List<Coord> mergePoints = new ArrayList<>(); // first add all roads with their start and end points to the // start/endpoint lists for (ConvertedWay road : roadsToMerge) { List<Coord> points = road.getWay().getPoints(); Coord start = points.get(0); Coord end = points.get(points.size() - 1); if (start == end) { // do not merge closed roads result.add(road); continue; } mergePoints.add(start); mergePoints.add(end); startPoints.add(start, road); endPoints.add(end, road); } workoutRestrictionRelations(restrictions); workoutThroughRoutes(throughRouteRelations); // a set of all points where no more merging is possible Set<Coord> mergeCompletedPoints = Collections.newSetFromMap(new IdentityHashMap<Coord, Boolean>()); // go through all start/end points and check if a merge is possible for (Coord mergePoint : mergePoints) { if (mergeCompletedPoints.contains(mergePoint)) { // a previous run did not show any possible merge // do not check again continue; } // get all road that start with the merge point List<ConvertedWay> startRoads = startPoints.get(mergePoint); // get all roads that end with the merge point List<ConvertedWay> endRoads = endPoints.get(mergePoint); if (endRoads.isEmpty() || startRoads.isEmpty()) { // this might happen if another merge operation changed endPoints and/or startPoints mergeCompletedPoints.add(mergePoint); continue; } // go through all combinations and test which combination is the best double bestAngle = Double.MAX_VALUE; ConvertedWay mergeRoad1 = null; ConvertedWay mergeRoad2 = null; for (ConvertedWay road1 : endRoads) { // check if the road has a restriction at the merge point // which does not allow us to merge the road at this point if (hasRestriction(mergePoint, road1.getWay())) { continue; } List<Coord> points1 = road1.getWay().getPoints(); // go through all candidates to merge for (ConvertedWay road2 : startRoads) { if (hasRestriction(mergePoint, road2.getWay())) { continue; } List<Coord> points2 = road2.getWay().getPoints(); // the second road is merged into the first road // so only the id of the first road is kept // This also means that the second road must not have a restriction on // both start and end point if (hasRestriction(points2.get(points2.size()-1), road2.getWay())) { continue; } // check if both roads can be merged if (isMergeable(mergePoint, road1, road2)){ // yes they might be merged // calculate the angle between them // if there is more then one road to merge the one with the lowest angle is merged double angle = Math.abs(Utils.getAngle(points1.get(points1.size()-2), mergePoint, points2.get(1))); log.debug("Road",road1.getWay().getId(),"and road",road2.getWay().getId(),"are mergeable with angle",angle); if (angle < bestAngle) { mergeRoad1 = road1; mergeRoad2 = road2; bestAngle = angle; } } } } // is there a pair of roads that can be merged? if (mergeRoad1 != null && mergeRoad2 != null) { // yes!! => merge them log.debug("Merge",mergeRoad1.getWay().getId(),"and",mergeRoad2.getWay().getId(),"with angle",bestAngle); mergeRoads(mergeRoad1, mergeRoad2); noMerges++; } else { // no => do not check again this point again mergeCompletedPoints.add(mergePoint); } } // copy all merged roads to the roads list for (List<ConvertedWay> mergedRoads : endPoints.values()) { result.addAll(mergedRoads); } // sort the roads to ensure that the order of roads is constant for two runs Collections.sort(result, new Comparator<ConvertedWay>() { public int compare(ConvertedWay o1, ConvertedWay o2) { return Integer.compare(o1.getIndex(), o2.getIndex()); } }); // print out some statistics int noRoadsAfterMerge = result.size(); log.info("Roads before/after merge:", noRoadsBeforeMerge, "/", noRoadsAfterMerge); int percentage = (int) Math.round((noRoadsBeforeMerge - noRoadsAfterMerge) * 100.0d / noRoadsBeforeMerge); log.info("Road network reduced by", percentage, "%",noMerges,"merges"); return result; } /** * Checks if the given {@code otherRoad} can be merged with this road at * the given {@code mergePoint}. * @param mergePoint the coord where this road and otherRoad might be merged * @param road1 1st road instance * @param road2 2nd road instance * @return {@code true} road1 can be merged with {@code road2}; * {@code false} the roads cannot be merged at {@code mergePoint} */ private static boolean isMergeable(Coord mergePoint, ConvertedWay road1, ConvertedWay road2) { // check if basic road attributes match if (road1.getRoadClass() != road2.getRoadClass()) return false; if (road1.getRoadSpeed() != road2.getRoadSpeed()) return false; Way way1 = road1.getWay(); Way way2 = road2.getWay(); if (road1.getAccess() != road2.getAccess()) { if (log.isDebugEnabled()) { reportFirstDifferentTag(way1, way2, road1.getAccess(), road2.getAccess(), AccessTagsAndBits.ACCESS_TAGS); } return false; } if (road1.getRouteFlags() != road2.getRouteFlags()) { if (log.isDebugEnabled()) { reportFirstDifferentTag(way1, way2, road1.getRouteFlags(), road2.getRouteFlags(), AccessTagsAndBits.ROUTE_TAGS); } return false; } // now check if this road starts or stops at the mergePoint Coord cStart = road1.getWay().getPoints().get(0); Coord cEnd = road1.getWay().getPoints().get(road1.getWay().getPoints().size() - 1); if (cStart != mergePoint && cEnd != mergePoint) { // it doesn't => roads not mergeable at mergePoint return false; } // do the same check for the otherRoad Coord cOtherStart = way2.getPoints().get(0); Coord cOtherEnd = way2.getPoints() .get(way2.getPoints().size() - 1); if (cOtherStart != mergePoint && cOtherEnd != mergePoint) { // otherRoad does not start or stop at mergePoint => // roads not mergeable at mergePoint return false; } // check if merging would create a closed way - which should not // be done (why? WanMil) if (cStart == cOtherEnd) { return false; } // check if certain fields in the GType objects are the same if (isGTypeMergeable(road1.getGType(), road2.getGType()) == false) { return false; } if (road1.isOneway()){ assert road2.isOneway(); // oneway must not only be checked for equality // but also for correct direction of both ways if ((cStart == mergePoint) == (cOtherStart == mergePoint)) { // both ways are oneway but they have a different direction log.warn("oneway with different direction", way1.getId(),way2.getId()); return false; } } // checks if the tag values of both ways match so that the ways // can be merged if (isWayMergeable(mergePoint, way1, way2) == false) return false; // check if the angle between the two ways is not too sharp if (isAngleOK(mergePoint, way1, way2) == false) return false; return true; } /** * For logging purposes. Print first tag with different meaning. * @param way1 1st way * @param way2 2nd way * @param flags1 the bit mask for 1st way * @param flags2 the bit mask for 2nd way * @param tagMaskMap the map that explains the meaning of the bit masks */ private static void reportFirstDifferentTag(Way way1, Way way2, byte flags1, byte flags2, Map<String, Byte> tagMaskMap) { for (Entry<String, Byte> entry : tagMaskMap.entrySet()){ byte mask = entry.getValue(); if ((flags1 & mask) != (flags2 & mask)){ String tagKey = entry.getKey(); log.debug(entry.getKey(), "does not match", way1.getId(), "(" + way1.getTag(tagKey) + ")", way2.getId(), "(" + way2.getTag(tagKey) + ")"); return; // report only first mismatch } } } /** * Checks if two GType objects can be merged. Not all fields are compared. * @param type1 the 1st GType * @param type2 the 2nd GType * @return {@code true} both GType objects can be merged; {@code false} GType * objects do not match and must not be merged */ private static boolean isGTypeMergeable(GType type1, GType type2) { if (type1.getType() != type2.getType()) { return false; } if (type1.getMinResolution() != type2.getMinResolution()) { return false; } if (type1.getMaxResolution() != type2.getMaxResolution()) { return false; } if (type1.getMinLevel() != type2.getMinLevel()) { return false; } if (type1.getMaxLevel() != type2.getMaxLevel()) { return false; } // roadClass and roadSpeed are taken from the ConvertedWay objects // //default name is applied before the RoadMerger starts //so they needn't be equal // if (stringEquals(gtype.getDefaultName(), // otherGType.getDefaultName()) == false) { // return false; // } // log.info("Matches"); return true; } /** * Checks if the tag values of the {@link Way} objects of both roads * match so that both roads can be merged. * @param mergePoint the coord where both roads should be merged * @param way1 1st way * @param way2 2nd way * @return {@code true} tag values match so that both roads might be merged; * {@code false} tag values differ so that road must not be merged */ private static boolean isWayMergeable(Coord mergePoint, Way way1, Way way2) { // tags that need to have an equal value for (String tagname : mergeTagsEqualValue) { String tag1 = way1.getTag(tagname); String tag2 = way2.getTag(tagname); if (stringEquals(tag1, tag2) == false) { if (log.isDebugEnabled()){ log.debug(tagname, "does not match", way1.getId(), "(" + tag1 + ")", way2.getId(), "(" + tag2 + ")"); } return false; } } return true; } /** * Checks if the angle between the two {@link Way} objects of both roads * is not too sharp so that both roads can be merged. * @param mergePoint the coord where both roads should be merged * @param way1 1st way * @param way2 2nd way * @return {@code true} angle is okay, roads might be merged; * {@code false} angle is so sharp that roads must not be merged */ private static boolean isAngleOK(Coord mergePoint, Way way1, Way way2) { // Check the angle of the two ways Coord cOnWay1; if (way1.getPoints().get(0) == mergePoint) { cOnWay1 = way1.getPoints().get(1); } else { cOnWay1 = way1.getPoints().get(way1.getPoints().size() - 2); } Coord cOnWay2; if (way2.getPoints().get(0) == mergePoint) { cOnWay2 = way2.getPoints().get(1); } else { cOnWay2 = way2.getPoints().get( way2.getPoints().size() - 2); } double angle = Math.abs(Utils.getAngle(cOnWay1, mergePoint, cOnWay2)); if (angle > MAX_MERGE_ANGLE) { // The angle exceeds the limit => do not merge // Don't know if this is really required or not. // But the number of merges which do not succeed due to this // restriction is quite low and there have been requests // for this: http://www.mkgmap.org.uk/pipermail/mkgmap-dev/2013q3/018649.html log.info("Do not merge ways",way1.getId(),"and",way2.getId(),"because they span a too big angle",angle,"°"); return false; } return true; } }