/* * Copyright (C) 2007 Steve Ratcliffe * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License 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. * * * Author: Steve Ratcliffe * Create date: Feb 17, 2008 */ package uk.me.parabola.mkgmap.osmstyle; import it.unimi.dsi.fastutil.shorts.ShortArrayList; import java.util.ArrayList; import java.util.Arrays; import java.util.BitSet; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.logging.Level; import uk.me.parabola.imgfmt.ExitException; import uk.me.parabola.imgfmt.app.Area; import uk.me.parabola.imgfmt.app.Coord; import uk.me.parabola.imgfmt.app.CoordNode; import uk.me.parabola.imgfmt.app.Exit; import uk.me.parabola.imgfmt.app.Label; import uk.me.parabola.imgfmt.app.net.AccessTagsAndBits; import uk.me.parabola.imgfmt.app.net.GeneralRouteRestriction; import uk.me.parabola.imgfmt.app.trergn.ExtTypeAttributes; import uk.me.parabola.imgfmt.app.trergn.MapObject; import uk.me.parabola.log.Logger; import uk.me.parabola.mkgmap.build.LocatorConfig; import uk.me.parabola.mkgmap.build.LocatorUtil; import uk.me.parabola.mkgmap.filters.LineSizeSplitterFilter; import uk.me.parabola.mkgmap.general.AreaClipper; import uk.me.parabola.mkgmap.general.Clipper; import uk.me.parabola.mkgmap.general.LineAdder; import uk.me.parabola.mkgmap.general.LineClipper; import uk.me.parabola.mkgmap.general.MapCollector; import uk.me.parabola.mkgmap.general.MapElement; import uk.me.parabola.mkgmap.general.MapExitPoint; import uk.me.parabola.mkgmap.general.MapLine; import uk.me.parabola.mkgmap.general.MapPoint; import uk.me.parabola.mkgmap.general.MapRoad; import uk.me.parabola.mkgmap.general.MapShape; import uk.me.parabola.mkgmap.osmstyle.housenumber.HousenumberGenerator; import uk.me.parabola.mkgmap.reader.osm.CoordPOI; import uk.me.parabola.mkgmap.reader.osm.Element; import uk.me.parabola.mkgmap.reader.osm.FakeIdGenerator; import uk.me.parabola.mkgmap.reader.osm.FeatureKind; import uk.me.parabola.mkgmap.reader.osm.GType; import uk.me.parabola.mkgmap.reader.osm.Node; import uk.me.parabola.mkgmap.reader.osm.OsmConverter; import uk.me.parabola.mkgmap.reader.osm.Relation; import uk.me.parabola.mkgmap.reader.osm.RestrictionRelation; import uk.me.parabola.mkgmap.reader.osm.Rule; import uk.me.parabola.mkgmap.reader.osm.Style; import uk.me.parabola.mkgmap.reader.osm.TagDict; import uk.me.parabola.mkgmap.reader.osm.TypeResult; import uk.me.parabola.mkgmap.reader.osm.Way; import uk.me.parabola.util.EnhancedProperties; import uk.me.parabola.util.MultiHashMap; /** * Convert from OSM to the mkgmap intermediate format using a style. * A style is a collection of files that describe the mappings to be used * when converting. * * @author Steve Ratcliffe */ public class StyledConverter implements OsmConverter { private static final Logger log = Logger.getLogger(StyledConverter.class); private static final Logger roadLog = Logger.getLogger(StyledConverter.class.getName()+".roads"); private final ShortArrayList nameTagList; private final MapCollector collector; private Clipper clipper = Clipper.NULL_CLIPPER; private Area bbox = new Area(-90.0d, -180.0d, 90.0d, 180.0d); // default is planet private final List<RestrictionRelation> restrictions = new ArrayList<>(); private final MultiHashMap<Long, RestrictionRelation> wayRelMap = new MultiHashMap<>(); private Map<Node, List<Way>> poiRestrictions = new LinkedHashMap<>(); private final List<Relation> throughRouteRelations = new ArrayList<>(); // limit line length to avoid problems with portions of really // long lines being assigned to the wrong subdivision private static final int MAX_LINE_LENGTH = 40000; // limit arc lengths to what can be handled by RouteArc private static final int MAX_ARC_LENGTH = 20450000; // (1 << 22) * 16 / 3.2808 ~ 20455030*/ private static final int MAX_NODES_IN_WAY = 64; // possibly could be increased // nodeIdMap maps a Coord into a CoordNode private IdentityHashMap<Coord, CoordNode> nodeIdMap = new IdentityHashMap<>(); public final static String WAY_POI_NODE_IDS = "mkgmap:way-poi-node-ids"; private final HashMap<Integer, Map<String,MapPoint>> pointMap; private List<ConvertedWay> roads = new ArrayList<>(); private List<ConvertedWay> lines = new ArrayList<>(); private HashMap<Long, ConvertedWay> modifiedRoads = new HashMap<>(); private HashSet<Long> deletedRoads = new HashSet<>(); private int nextNodeId = 1; private int nextRoadId = 1; private HousenumberGenerator housenumberGenerator; private final Rule wayRules; private final Rule nodeRules; private final Rule lineRules; private final Rule polygonRules; private Style style; private String driveOn; private Boolean driveOnLeft; private int numDriveOnLeftRoads; private int numDriveOnRightRoads; private int numDriveOnSideUnknown; private int numRoads; private String countryAbbr; private final boolean checkRoundabouts; private int reportDeadEnds; private final boolean linkPOIsToWays; private final boolean mergeRoads; private final boolean routable; private LineAdder lineAdder = new LineAdder() { public void add(MapLine element) { if (element instanceof MapRoad){ collector.addRoad((MapRoad) element); } else collector.addLine(element); } }; public StyledConverter(Style style, MapCollector collector, EnhancedProperties props) { this.collector = collector; List<String> nameTags = LocatorUtil.getNameTags(props); if (nameTags != null){ nameTagList = new ShortArrayList(); for(String n : nameTags) nameTagList.add(TagDict.getInstance().xlate(n)); } else nameTagList = null; this.style = style; pointMap = new HashMap<>(); wayRules = style.getWayRules(); nodeRules = style.getNodeRules(); lineRules = style.getLineRules(); polygonRules = style.getPolygonRules(); // perform legacy test, older versions of mkgmap used to set mkgmap:dest_hint=true // newer version will set it to a reasonable destination string if (lineRules.containsExpression("$mkgmap:dest_hint='true'")){ log.error("At least one 'lines' rule in the style contains the expression mkgmap:dest_hint=true, it should be changed to mkgmap:dest_hint=*"); } housenumberGenerator = new HousenumberGenerator(props); driveOn = props.getProperty("drive-on", null); if (driveOn == null) driveOn = "detect,right"; switch (driveOn) { case "left": driveOnLeft = true; break; case "right": driveOnLeft = false; break; case "detect": case "detect,left": case "detect,right": break; default: throw new ExitException("invalid parameters for option drive-on:"+driveOn); } countryAbbr = props.getProperty("country-abbr", null); if (countryAbbr != null) countryAbbr = countryAbbr.toUpperCase(); checkRoundabouts = props.getProperty("check-roundabouts",false); reportDeadEnds = props.getProperty("report-dead-ends", 1); LineAdder overlayAdder = style.getOverlays(lineAdder); if (overlayAdder != null) lineAdder = overlayAdder; linkPOIsToWays = props.getProperty("link-pois-to-ways", false); // undocumented option - usually used for debugging only mergeRoads = props.getProperty("no-mergeroads", false) == false; routable = props.containsKey("route"); } /** One type result for ways to avoid recreating one for each way. */ private final WayTypeResult wayTypeResult = new WayTypeResult(); private class WayTypeResult implements TypeResult { private Way way; /** flag if the rule was fired */ private boolean matched; public void setWay(Way way) { this.way = way; this.matched = false; } public void add(Element el, GType type) { this.matched = true; if (type.isContinueSearch()) { // If not already copied, do so now if (el == way) el = way.copy(); } postConvertRules(el, type); if (type.isRoad() == false) housenumberGenerator.addWay((Way)el); addConvertedWay((Way) el, type); } /** * Retrieves if a rule of the style matched and the way is converted. * @return {@code true} way is converted; {@code false} way is not converted */ public boolean isMatched() { return matched; } } /** * This takes the way and works out what kind of map feature it is and makes * the relevant call to the mapper callback. * <p> * As a few examples we might want to check for the 'highway' tag, work out * if it is an area of a park etc. * * @param way The OSM way. */ private final static short styleFilterTagKey = TagDict.getInstance().xlate("mkgmap:stylefilter"); private final static short makeCycleWayTagKey = TagDict.getInstance().xlate("mkgmap:make-cycle-way"); private long lastRoadId = 0; private int lineCacheId = 0; private BitSet routingWarningWasPrinted = new BitSet(); public void convertWay(final Way way) { if (way.getPoints().size() < 2 || way.getTagCount() == 0){ // no tags or no points => nothing to convert removeRestrictionsWithWay(Level.WARNING, way, "is ignored"); return; } preConvertRules(way); String styleFilterTag = way.getTag(styleFilterTagKey); Rule rules; if ("polyline".equals(styleFilterTag)) rules = lineRules; else if ("polygon".equals(styleFilterTag)) rules = polygonRules; else { if (way.isClosedInOSM() && !way.isComplete() && !way.hasIdenticalEndPoints()) way.getPoints().add(way.getPoints().get(0)); if (way.hasIdenticalEndPoints() == false || way.getPoints().size() < 4) rules = lineRules; else rules = wayRules; } Way cycleWay = null; String cycleWayTag = way.getTag(makeCycleWayTagKey); if ("yes".equals(cycleWayTag)){ way.deleteTag("mkgmap:make-cycle-way"); cycleWay = makeCycleWay(way); way.addTag("bicycle", "no"); // make sure that bicycles are using the added bicycle way } wayTypeResult.setWay(way); lineCacheId = rules.resolveType(lineCacheId, way, wayTypeResult); if (wayTypeResult.isMatched() == false) { // no match found but we have to keep it for house number processing housenumberGenerator.addWay(way); } if (cycleWay != null){ wayTypeResult.setWay(cycleWay); lineCacheId = rules.resolveType(lineCacheId, cycleWay, wayTypeResult); if (wayTypeResult.isMatched() == false) { // no match found but we have to keep it for house number processing housenumberGenerator.addWay(cycleWay); } } if (lastRoadId != way.getId()){ // this way was not added to the roads list removeRestrictionsWithWay(Level.WARNING, way, "is not routable"); } else { // way was added as road, check if we also have non-routable lines for the way // which have to be skipped by WrongAngleFixer for (int i = lines.size()-1; i >= 0; --i){ ConvertedWay cw = lines.get(i); if (cw.getWay().getId() == way.getId()){ cw.setOverlay(true); int lineType = cw.getGType().getType(); if (GType.isSpecialRoutableLineType(lineType) && cw.getGType().getMinLevel() == 0){ if (!routingWarningWasPrinted.get(lineType)){ log.error("routable type", GType.formatType(cw.getGType().getType()), "is used with a non-routable way which was also added as a routable way. This leads to routing errors.", "Try --check-styles to check the style."); routingWarningWasPrinted.set(lineType); } } } else break; } } } private int lineIndex = 0; private final static short onewayTagKey = TagDict.getInstance().xlate("oneway"); private void addConvertedWay(Way way, GType foundType) { if (foundType.getFeatureKind() == FeatureKind.POLYGON){ addShape(way, foundType); return; } boolean wasReversed = false; String oneWay = way.getTag(onewayTagKey); if (oneWay != null){ if("-1".equals(oneWay) || "reverse".equals(oneWay)) { // it's a oneway street in the reverse direction // so reverse the order of the nodes and change // the oneway tag to "yes" way.reverse(); wasReversed = true; way.addTag(onewayTagKey, "yes"); } if (way.tagIsLikeYes(onewayTagKey)) { way.addTag(onewayTagKey, "yes"); if (foundType.isRoad() && checkFixmeCoords(way) ) way.addTag("mkgmap:dead-end-check", "false"); } else way.deleteTag(onewayTagKey); } ConvertedWay cw = new ConvertedWay(lineIndex++, way, foundType); cw.setReversed(wasReversed); if (cw.isRoad()){ roads.add(cw); numRoads++; if (cw.isFerry() == false){ String country = way.getTag(countryTagKey); if (country != null) { boolean drivingSideIsLeft =LocatorConfig.get().getDriveOnLeftFlag(country); if (drivingSideIsLeft) numDriveOnLeftRoads++; else numDriveOnRightRoads++; if (driveOnLeft != null && drivingSideIsLeft != driveOnLeft) log.warn("wrong driving side",way.toBrowseURL()); if (log.isDebugEnabled()) log.debug("assumed driving side is",(drivingSideIsLeft ? "left" : "right"),way.toBrowseURL()); } else numDriveOnSideUnknown++; } if (cw.isRoundabout()) { if (wasReversed) log.warn("Roundabout", way.getId(), "has reverse oneway tag (" + way.getPoints().get(0).toOSMURL() + ")"); } lastRoadId = way.getId(); } else lines.add(cw); } /** One type result for nodes to avoid recreating one for each node. */ private NodeTypeResult nodeTypeResult = new NodeTypeResult(); private class NodeTypeResult implements TypeResult { private Node node; /** flag if the rule was fired */ private boolean matched; public void setNode(Node node) { this.node = node; this.matched = false; } public void add(Element el, GType type) { this.matched = true; if (type.isContinueSearch()) { // If not already copied, do so now if (el == node) el = node.copy(); } postConvertRules(el, type); housenumberGenerator.addNode((Node)el); addPoint((Node) el, type); } /** * Retrieves if a rule of the style matched and the node is converted. * @return {@code true} node is converted; {@code false} node is not converted */ public boolean isMatched() { return matched; } } /** * Takes a node (that has its own identity) and converts it from the OSM * type to the Garmin map type. * * @param node The node to convert. */ public void convertNode(final Node node) { if (node.getTagCount() == 0) { // no tags => nothing to convert return; } preConvertRules(node); nodeTypeResult.setNode(node); nodeRules.resolveType(node, nodeTypeResult); if (nodeTypeResult.isMatched() == false) { // no match found but we have to keep it for house number processing housenumberGenerator.addNode(node); } } private static final short nameTagKey = TagDict.getInstance().xlate("name"); /** * Rules to run before converting the element. */ private void preConvertRules(Element el) { if (nameTagList == null) return; for (short tagKey : nameTagList) { String val = el.getTag(tagKey); if (val != null) { if (tagKey != nameTagKey) { // add or replace name el.addTag(nameTagKey, val); } break; } } } /** * Construct a cycleway that has the same points as an existing way. Used for separate * cycle lanes. * @param way The original way. * @return The new way, which will have the same points and have suitable cycle tags. */ private static Way makeCycleWay(Way way) { Way cycleWay = new Way(way.getId(), way.getPoints()); cycleWay.copyTags(way); cycleWay.addTag("access", "no"); cycleWay.addTag("bicycle", "yes"); cycleWay.addTag("mkgmap:synthesised", "yes"); cycleWay.addTag(onewayTagKey, "no"); // remove explicit access tags cycleWay.deleteTag("foot"); cycleWay.deleteTag("motorcar"); cycleWay.deleteTag("goods"); cycleWay.deleteTag("hgv"); cycleWay.deleteTag("bus"); cycleWay.deleteTag("taxi"); cycleWay.deleteTag("emergency"); cycleWay.deleteTag("vehicle"); cycleWay.deleteTag("motor_vehicle"); cycleWay.deleteTag("carpool"); cycleWay.deleteTag("motorcycle"); cycleWay.deleteTag("psv"); cycleWay.deleteTag("truck"); return cycleWay; } /** * Built in rules to run after converting the element. */ private static void postConvertRules(Element el, GType type) { // Set the default_name if no name is set if (type.getDefaultName() != null && el.getName() == null) el.addTag("mkgmap:label:1", type.getDefaultName()); } /** * Set the bounding box for this map. This should be set before any other * elements are converted if you want to use it. All elements that are added * are clipped to this box, new points are added as needed at the boundary. * * If a node or a way falls completely outside the boundary then it would be * omitted. This would not normally happen in the way this option is typically * used however. * * @param bbox The bounding area, must not be null. */ public void setBoundingBox(Area bbox) { this.clipper = new AreaClipper(bbox); this.bbox = bbox; // we calculate our own bounding box, now let the collector know about it. collector.addToBounds(new Coord(bbox.getMinLat(), bbox.getMinLong())); collector.addToBounds(new Coord(bbox.getMaxLat(), bbox.getMaxLong())); } /** * Remove all restriction relations that are invalid if the way will not appear * in the NOD file. * @param logLevel * @param way the way that was removed * @param reason explanation for the removal */ private void removeRestrictionsWithWay(Level logLevel, Way way, String reason){ List<RestrictionRelation> rrList = wayRelMap.get(way.getId()); for (RestrictionRelation rr : rrList){ if (rr.isValidWithoputWay(way.getId()) == false){ if (log.isLoggable(logLevel)){ log.log(logLevel, "restriction",rr.toBrowseURL()," is ignored because referenced way",way.toBrowseURL(),reason); } restrictions.remove(rr); } } } /** * Merges roads with identical attributes (GType, OSM tags) to reduce the size of the * road network. */ private void mergeRoads() { if (mergeRoads == false) { log.info("Merging roads is disabled"); return; } RoadMerger merger = new RoadMerger(); roads = merger.merge(roads, restrictions, throughRouteRelations); } public void end() { pointMap.clear(); style.reportStats(); driveOnLeft = calcDrivingSide(); setHighwayCounts(); findUnconnectedRoads(); rotateClosedWaysToFirstNode(); filterCoordPOI(); WrongAngleFixer wrongAngleFixer = new WrongAngleFixer(bbox); wrongAngleFixer.optimizeWays(roads, lines, modifiedRoads, deletedRoads, restrictions); // make sure that copies of modified roads have equal points for (ConvertedWay line : lines){ if (!line.isValid()) continue; Way way = line.getWay(); if (deletedRoads.contains(way.getId())){ line.getPoints().clear(); continue; } if (!line.isOverlay()) continue; ConvertedWay modWay = modifiedRoads.get(way.getId()); if (modWay != null){ List<Coord> points = line.getPoints(); points.clear(); points.addAll(modWay.getPoints()); if (modWay.isReversed() != line.isReversed()) Collections.reverse(points); } } for (Long wayId: deletedRoads){ if (wayRelMap.containsKey(wayId)){ // may happen e.g. when very short way is leading to nowhere log.warn("Way that is used in valid restriction relation was removed, id:",wayId); } } deletedRoads = null; modifiedRoads = null; mergeRoads(); resetHighwayCounts(); setHighwayCounts(); for (ConvertedWay cw : lines){ if (cw.isValid()) addLine(cw.getWay(), cw.getGType()); } lines = null; if (roadLog.isInfoEnabled()) { roadLog.info("Flags: oneway,no-emergency, no-delivery, no-throughroute, no-truck, no-bike, no-foot, carpool, no-taxi, no-bus, no-car"); roadLog.info(String.format("%19s %4s %11s %6s %s", "Road-OSM-Id","Type","Flags", "Points", "Labels")); } // add the roads after the other lines for (ConvertedWay cw : roads){ if (cw.isValid()) addRoad(cw); } housenumberGenerator.generate(lineAdder, nextNodeId); housenumberGenerator = null; if (routable) createRouteRestrictionsFromPOI(); poiRestrictions = null; if (routable){ for (RestrictionRelation rr : restrictions) { rr.addRestriction(collector, nodeIdMap); } } roads = null; if (routable){ 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"); } } CoordNode coordNode = null; if(node == null) log.warn("Through route relation", relation.toBrowseURL(), "is missing the junction node"); else { Coord junctionPoint = node.getLocation(); if(bbox != null && !bbox.contains(junctionPoint)) { // junction is outside of the tile - ignore it continue; } coordNode = nodeIdMap.get(junctionPoint); if(coordNode == null) log.warn("Through route relation", relation.toBrowseURL(), "junction node at", junctionPoint.toOSMURL(), "is not a routing node"); } if(w1 == null || w2 == null) log.warn("Through route relation", relation.toBrowseURL(), "should reference 2 ways that meet at the junction node"); if(coordNode != null && w1 != null && w2 != null) collector.addThroughRoute(coordNode.getId(), w1.getId(), w2.getId()); } } // return memory to GC nodeIdMap = null; throughRouteRelations.clear(); restrictions.clear(); } /** * Check the counters and verify the driveOn value to calculate * the drive on left flag. */ private Boolean calcDrivingSide() { Boolean dol = null; log.info("Found", numRoads, "roads", numDriveOnLeftRoads, "in drive-on-left country,", numDriveOnRightRoads, "in drive-on-right country, and", numDriveOnSideUnknown, " with unknwon country"); if (numDriveOnLeftRoads> 0 && numDriveOnRightRoads > 0) log.error("Attention: Tile contains both drive-on-left (" + numDriveOnLeftRoads + ") and drive-on-right roads (" + numDriveOnRightRoads + ")"); if (driveOn.startsWith("detect")) { if (numDriveOnSideUnknown > numRoads * 0.05){ // warn if more than 5% of the roads are in unknown area log.warn("Found", numDriveOnSideUnknown, "roads with unknown country and driving side"); } if (numDriveOnLeftRoads > numDriveOnRightRoads + numDriveOnSideUnknown) { dol = true; } else if (numDriveOnRightRoads > numDriveOnLeftRoads + numDriveOnSideUnknown) { dol = false; } else { if (driveOn.endsWith("left")) dol = true; else dol = false; } log.info("detected value for driving on left flag is:",dol); } else { dol = ("left".equals(driveOn)); // warn if user given flag is obviously wrong if ("left".equals(driveOn) && numDriveOnLeftRoads == 0 && numDriveOnRightRoads > 0) log.warn("The drive-on-left flag is set but tile contains only drive-on-right roads"); if ("right".equals(driveOn) && numDriveOnRightRoads == 0 && numDriveOnLeftRoads > 0) log.warn("The drive-on-left flag is NOT set used but tile contains only drive-on-left roads"); } if (dol == null) dol = false; // should not happen return dol; } /** * Try to make sure that closed ways start with a point that is * also part of another road. This reduces the number of nodes * a little bit. * */ private void rotateClosedWaysToFirstNode() { for (ConvertedWay cw: roads){ if (!cw.isValid()) continue; Way way = cw.getWay(); List<Coord> points = way.getPoints(); if (points.size() < 3) continue; if (points.get(0) != points.get(points.size()-1)) continue; // this is a closed way Coord p0 = points.get(0); if (p0.getHighwayCount() > 2) continue; for (int i = 1; i < points.size() - 1;i++){ Coord p = points.get(i); if (p.getHighwayCount() > 1){ p.incHighwayCount(); // this will be the new first + last point // first point connects only last point, remove last points.remove(points.size()-1); p0.decHighwayCount(); Collections.rotate(points, -i); points.add(points.get(0)); // close again modifiedRoads.put(way.getId(), cw); break; } } } } /** * Check if roundabout has correct direction. Set driveOnRight or * driveOnLeft is not yet set. * */ private void checkRoundabout(ConvertedWay cw) { if (cw.isRoundabout() == false) return; Way way = cw.getWay(); List<Coord> points = way.getPoints(); // if roundabout checking is enabled and roundabout has at // least 3 points and it has not been marked as "don't // check", check its direction if (checkRoundabouts && points.size() > 2 && !way.tagIsLikeYes("mkgmap:no-dir-check") && !way.tagIsLikeNo("mkgmap:dir-check")) { Coord centre = way.getCofG(); int dir = 0; // check every third segment for (int i = 0; (i + 1) < points.size(); i += 3) { Coord pi = points.get(i); Coord pi1 = points.get(i + 1); // TODO: check if high prec coords allow to use smaller // distance // don't check segments that are very short if (pi.distance(centre) > 2.5 && pi.distance(pi1) > 2.5) { // determine bearing from segment that starts with // point i to centre of roundabout double a = pi.bearingTo(pi1); double b = pi.bearingTo(centre) - a; while (b > 180) b -= 360; while (b < -180) b += 360; // if bearing to centre is between 15 and 165 // degrees consider it trustworthy if (b >= 15 && b < 165) ++dir; else if (b <= -15 && b > -165) --dir; } } if (dir == 0) log.info("Roundabout segment " + way.getId() + " direction unknown (see " + points.get(0).toOSMURL() + ")"); else { boolean clockwise = dir > 0; if (points.get(0) == points.get(points.size() - 1)) { // roundabout is a loop if (driveOnLeft == true && !clockwise || driveOnLeft == false && clockwise) { log.warn("Roundabout " + way.getId() + " direction is wrong - reversing it (see " + centre.toOSMURL() + ")"); way.reverse(); } } else if (driveOnLeft == true && !clockwise || driveOnLeft == false && clockwise) { // roundabout is a line log.warn("Roundabout segment " + way.getId() + " direction looks wrong (see " + points.get(0).toOSMURL() + ")"); } } } } /** * If POI changes access restrictions (e.g. bollards), create corresponding * route restrictions so that only allowed vehicles/pedestrians are routed * through this point. */ private void createRouteRestrictionsFromPOI() { Iterator<Map.Entry<Node, List<Way>>> iter = poiRestrictions.entrySet().iterator(); while (iter.hasNext()){ Map.Entry<Node, List<Way>> entry = iter.next(); Node node = entry.getKey(); Coord p = node.getLocation(); // list of ways that are connected to the poi List<Way> wayList = entry.getValue(); byte exceptMask = AccessTagsAndBits.evalAccessTags(node); Map<Integer,CoordNode> otherNodeIds = new LinkedHashMap<>(); CoordNode viaNode = null; boolean viaIsUnique = true; for (Way way : wayList) { CoordNode lastNode = null; for (Coord co: way.getPoints()){ // not 100% fail safe: points may have been replaced before if (co instanceof CoordNode == false) continue; CoordNode cn = (CoordNode) co; if (p.highPrecEquals(cn)){ if (viaNode == null) viaNode = cn; else if (viaNode != cn){ log.error("Found multiple points with equal coords as CoordPOI at " + p.toOSMURL()); // if we ever get here we can add code to identify the exact node viaIsUnique = false; } if (lastNode != null) otherNodeIds.put(lastNode.getId(),lastNode); } else { if (p.highPrecEquals(lastNode)) otherNodeIds.put(cn.getId(),cn); } lastNode = cn; } } if (viaNode == null){ log.error("Did not find CoordPOI node at " + p.toOSMURL() + " in ways " + wayList); continue; } if (viaIsUnique == false){ log.error("Found multiple points with equal coords as CoordPOI at " + p.toOSMURL()); continue; } if (otherNodeIds.size() < 2){ log.info("Access restriction in POI node " + node.toBrowseURL() + " was ignored, has no effect on any connected way"); continue; } GeneralRouteRestriction rr = new GeneralRouteRestriction("no_through", exceptMask, "CoordPOI at " + p.toOSMURL()); rr.setViaNodes(Arrays.asList(viaNode)); int added = collector.addRestriction(rr); if (added == 0){ log.info("Access restriction in POI node " + node.toBrowseURL() + " was ignored, has no effect on any connected way"); } else { log.info("Access restriction in POI node", node.toBrowseURL(), "was translated to",added,"route restriction(s)"); } if (wayList.size() > 1 && added > 2){ log.warn("Access restriction in POI node", node.toBrowseURL(), "affects routing on multiple ways"); } } } /** * Run the rules for this relation. As this is not an end object, then * the only useful rules are action rules that set tags on the contained * ways or nodes. Every rule should probably start with 'type=".."'. * * @param relation The relation to convert. */ public void convertRelation(Relation relation) { if (relation.getTagCount() == 0) { // no tags => nothing to convert return; } housenumberGenerator.addRelation(relation); // relation rules are not applied here because they are applied // earlier by the RelationStyleHook if(relation instanceof RestrictionRelation) { RestrictionRelation rr = (RestrictionRelation)relation; if(rr.isValid()) { restrictions.add(rr); for (long id : rr.getWayIds()) wayRelMap.add(id, rr); } } else if("through_route".equals(relation.getTag("type"))) { throughRouteRelations.add(relation); } } private void addLine(Way way, GType gt) { addLine(way, gt, -1); } private void addLine(Way way, GType gt, int replType) { List<Coord> wayPoints = way.getPoints(); List<Coord> points = new ArrayList<>(wayPoints.size()); double lineLength = 0; Coord lastP = null; for (Coord p : wayPoints) { if (p.highPrecEquals(lastP)) continue; points.add(p); if(lastP != null) { lineLength += p.distance(lastP); if(lineLength >= MAX_LINE_LENGTH) { if (log.isInfoEnabled()) log.info("Splitting line", way.toBrowseURL(), "at", p.toOSMURL(), "to limit its length to", (long)lineLength + "m"); addLine(way, gt, replType, points); points = new ArrayList<>(wayPoints.size() - points.size() + 1); points.add(p); lineLength = 0; } } lastP = p; } if(points.size() > 1) addLine(way, gt, replType, points); } private void addLine(Way way, GType gt, int replType, List<Coord> points) { MapLine line = new MapLine(); elementSetup(line, gt, way); if (replType >= 0) line.setType(replType); line.setPoints(points); if (way.tagIsLikeYes(onewayTagKey)) line.setDirection(true); clipper.clipLine(line, lineAdder); } private void addShape(Way way, GType gt) { // This is deceptively simple. At the time of writing, splitter only retains points that are within // the tile and some distance around it. Therefore a way that is closed in reality may not be closed // as we see it in its incomplete state. // if (!way.hasIdenticalEndPoints() && way.hasEqualEndPoints()) log.error("shape is not closed with identical points " + way.getId()); if (!way.hasIdenticalEndPoints()) return; // TODO: split self intersecting polygons? final MapShape shape = new MapShape(way.getId()); elementSetup(shape, gt, way); shape.setPoints(way.getPoints()); long areaVal = 0; String tagStringVal = way.getTag(drawLevelTagKey); if (tagStringVal != null) { try { areaVal = Integer.parseInt(tagStringVal); if (areaVal < 1 || areaVal > 100) { log.error("mkgmap:drawLevel must be in range 1..100, not", areaVal); areaVal = 0; } else if (areaVal <= 50) areaVal = Long.MAX_VALUE - areaVal; // 1 => MAX_VALUE-1, 50 => MAX_VALUE-50 else areaVal = 101 - areaVal; // 51 => 50, 100 => 1 } catch (NumberFormatException e) { log.error("mkgmap:drawLevel invalid integer:", tagStringVal); } } if (areaVal == 0) areaVal = way.getFullArea(); shape.setFullArea(areaVal); clipper.clipShape(shape, collector); } private void addPoint(Node node, GType gt) { if (!clipper.contains(node.getLocation())) return; // to handle exit points we use a subclass of MapPoint // to carry some extra info (a reference to the // motorway associated with the exit) MapPoint mp; int type = gt.getType(); if(type >= 0x2000 && type < 0x2800) { String ref = node.getTag(Exit.TAG_ROAD_REF); String id = node.getTag("mkgmap:osmid"); if(ref != null) { String to = node.getTag(Exit.TAG_TO); MapExitPoint mep = new MapExitPoint(ref, to); String fd = node.getTag(Exit.TAG_FACILITY); if(fd != null) mep.setFacilityDescription(fd); if(id != null) mep.setOSMId(id); mp = mep; } else { mp = new MapPoint(); if ("motorway_junction".equals(node.getTag("highway"))) log.warn("Motorway exit", node.getName(), "(" + node.toBrowseURL() + ") has no (motorway) ref! (either make the exit share a node with the motorway or specify the motorway ref with a", Exit.TAG_ROAD_REF, "tag)"); } } else { mp = new MapPoint(); } elementSetup(mp, gt, node); mp.setLocation(node.getLocation()); boolean dupPOI = checkDuplicatePOI(mp); if (dupPOI){ if (log.isInfoEnabled()){ if (FakeIdGenerator.isFakeId(node.getId())) log.info("ignmoring duplicate POI with type",GType.formatType(type),mp.getName(),"for generated element with id",node.getId(),"at",mp.getLocation().toDegreeString()); else log.info("ignmoring duplicate POI with type",GType.formatType(type),mp.getName(),"for element",node.toBrowseURL()); } return; } collector.addPoint(mp); } /** * Check if we already have added a point with the same type + name and equal location. * @param mp * @return */ private boolean checkDuplicatePOI(MapPoint mp) { Map<String, MapPoint> typeMap = pointMap.get(mp.getType()); if (typeMap == null){ typeMap = new HashMap<>(); pointMap.put(mp.getType(), typeMap); } MapPoint old = typeMap.get(mp.getName()); if (old == null){ typeMap.put(mp.getName(), mp); } else { if (old.getLocation().equals(mp.getLocation())) return true; } return false; } private static final short[] labelTagKeys = { TagDict.getInstance().xlate("mkgmap:label:1"), TagDict.getInstance().xlate("mkgmap:label:2"), TagDict.getInstance().xlate("mkgmap:label:3"), TagDict.getInstance().xlate("mkgmap:label:4"), }; private static final short highResOnlyTagKey = TagDict.getInstance().xlate("mkgmap:highest-resolution-only"); private static final short skipSizeFilterTagKey = TagDict.getInstance().xlate("mkgmap:skipSizeFilter"); private static final short drawLevelTagKey = TagDict.getInstance().xlate("mkgmap:drawLevel"); private static final short countryTagKey = TagDict.getInstance().xlate("mkgmap:country"); private static final short regionTagKey = TagDict.getInstance().xlate("mkgmap:region"); private static final short cityTagKey = TagDict.getInstance().xlate("mkgmap:city"); private static final short postal_codeTagKey = TagDict.getInstance().xlate("mkgmap:postal_code"); private static final short streetTagKey = TagDict.getInstance().xlate("mkgmap:street"); private static final short housenumberTagKey = TagDict.getInstance().xlate("mkgmap:housenumber"); private static final short phoneTagKey = TagDict.getInstance().xlate("mkgmap:phone"); private static final short is_inTagKey = TagDict.getInstance().xlate("mkgmap:is_in"); private static void elementSetup(MapElement ms, GType gt, Element element) { String[] labels = new String[4]; int noLabels = 0; for (int labelNo = 0; labelNo < 4; labelNo++) { String label1 = element.getTag(labelTagKeys[labelNo]); String label = Label.squashSpaces(label1); if (label != null) { labels[noLabels] = label; noLabels++; } } if (labels[0] != null) { ms.setLabels(labels); } ms.setType(gt.getType()); ms.setMinResolution(gt.getMinResolution()); ms.setMaxResolution(gt.getMaxResolution()); if (element.tagIsLikeYes(highResOnlyTagKey)){ ms.setMinResolution(ms.getMaxResolution()); } if (ms instanceof MapLine && element.tagIsLikeYes(skipSizeFilterTagKey)){ ((MapLine)ms).setSkipSizeFilter(true); } // Now try to get some address info for POIs String country = element.getTag(countryTagKey); String region = element.getTag(regionTagKey); String city = element.getTag(cityTagKey); String zip = element.getTag(postal_codeTagKey); String street = element.getTag(streetTagKey); String houseNumber = element.getTag(housenumberTagKey); String phone = element.getTag(phoneTagKey); String isIn = element.getTag(is_inTagKey); if(country != null) ms.setCountry(country); if(region != null) ms.setRegion(region); if(city != null) ms.setCity(city); if(zip != null) ms.setZip(zip); if(street != null) ms.setStreet(street); if(houseNumber != null) ms.setHouseNumber(houseNumber); if(isIn != null) ms.setIsIn(isIn); if(phone != null) ms.setPhone(phone); if(MapObject.hasExtendedType(gt.getType())) { // pass attributes with mkgmap:xt- prefix (strip prefix) Map<String,String> xta = element.getTagsWithPrefix("mkgmap:xt-", true); // also pass all attributes with seamark: prefix (no strip prefix) xta.putAll(element.getTagsWithPrefix("seamark:", false)); ms.setExtTypeAttributes(new ExtTypeAttributes(xta, "OSM id " + element.getId())); } } /** * Add a way to the road network. May call itself recursively and * might truncate the way if splitting is required. * @param way the way * @param gt the type assigned by the style */ private void addRoad(ConvertedWay cw) { Way way = cw.getWay(); if (way.getPoints().size() < 2){ log.warn("road has < 2 points",way.getId(),"(discarding)"); return; } checkRoundabout(cw); // process any Coords that have a POI associated with them final double stubSegmentLength = 25; // metres String wayPOI = way.getTag(WAY_POI_NODE_IDS); if (wayPOI != null) { List<Coord> points = way.getPoints(); // look for POIs that modify the way's road class or speed // or contain access restrictions // This could be e.g. highway=traffic_signals that reduces the // road speed to cause a short increase of travelling time // or a barrier for(int i = 0; i < points.size(); ++i) { Coord p = points.get(i); if (p instanceof CoordPOI && ((CoordPOI) p).isUsed()) { CoordPOI cp = (CoordPOI) p; Node node = cp.getNode(); if (wayPOI.contains("["+node.getId()+"]")){ log.debug("POI",node.getId(),"changes way",way.getId()); // make sure that we create nodes for all POI that // are converted to RouteRestrictions if(p.getHighwayCount() < 2 && cp.getConvertToViaInRouteRestriction() && (i != 0 && i != points.size()-1)) p.incHighwayCount(); String roadClass = node.getTag("mkgmap:road-class"); String roadSpeed = node.getTag("mkgmap:road-speed"); if(roadClass != null || roadSpeed != null) { // find good split point after POI Coord splitPoint; double segmentLength = 0; int splitPos = i+1; while( splitPos+1 < points.size()){ splitPoint = points.get(splitPos); segmentLength += splitPoint.distance(points.get(splitPos - 1)); if (splitPoint.getHighwayCount() > 1 || segmentLength > stubSegmentLength - 5) break; splitPos++; } if (segmentLength > stubSegmentLength + 10){ // insert a new point after the POI to // make a short stub segment splitPoint = points.get(splitPos); Coord prev = points.get(splitPos-1); double dist = splitPoint.distance(prev); double neededLength = stubSegmentLength - (segmentLength - dist); splitPoint = prev.makeBetweenPoint(splitPoint, neededLength / dist); double newDist = splitPoint.distance(prev); segmentLength += newDist - dist; splitPoint.incHighwayCount(); points.add(splitPos, splitPoint); } if((splitPos + 1) < points.size() && way.isViaWay() == false && safeToSplitWay(points, splitPos, i, points.size() - 1)) { Way tail = splitWayAt(way, splitPos); // recursively process tail of way addRoad(new ConvertedWay(cw, tail)); } boolean classChanged = cw.recalcRoadClass(node); if (classChanged && log.isInfoEnabled()){ log.info("POI changing road class of", way.toBrowseURL(), "to", cw.getRoadClass(), "at", points.get(0).toOSMURL()); } boolean speedChanged = cw.recalcRoadSpeed(node); if (speedChanged && log.isInfoEnabled()){ log.info("POI changing road speed of", way.toBrowseURL(), "to", cw.getRoadSpeed(), "at" , points.get(0).toOSMURL()); } } } } // if this isn't the last point in the way // and the next point modifies the way's speed/class, // split the way at this point to limit the size of // the affected region if (i + 1 < points.size() && points.get(i + 1) instanceof CoordPOI) { CoordPOI cp = (CoordPOI) points.get(i + 1); Node node = cp.getNode(); if (cp.isUsed() && wayPOI.contains("["+node.getId()+"]")){ if (node.getTag("mkgmap:road-class") != null || node.getTag("mkgmap:road-speed") != null) { // find good split point before POI double segmentLength = 0; int splitPos = i; Coord splitPoint; while( splitPos >= 0){ splitPoint = points.get(splitPos); segmentLength += splitPoint.distance(points.get(splitPos + 1)); if (splitPoint.getHighwayCount() >= 2 || segmentLength > stubSegmentLength - 5) break; --splitPos; } if (segmentLength > stubSegmentLength + 10){ // insert a new point before the POI to // make a short stub segment splitPoint = points.get(splitPos); Coord prev = points.get(splitPos+1); double dist = splitPoint.distance(prev); double neededLength = stubSegmentLength - (segmentLength - dist); splitPoint = prev.makeBetweenPoint(splitPoint, neededLength / dist); segmentLength += splitPoint.distance(prev) - dist; splitPoint.incHighwayCount(); splitPos++; points.add(splitPos, splitPoint); } if(splitPos > 0 && safeToSplitWay(points, splitPos, 0, points.size()-1)) { Way tail = splitWayAt(way, splitPos); // recursively process tail of way addRoad(new ConvertedWay(cw, tail)); } } } } } } // if there is a bounding box, clip the way with it List<Way> clippedWays = null; if(bbox != null) { List<List<Coord>> lineSegs = LineClipper.clip(bbox, way.getPoints()); if (lineSegs != null) { if (lineSegs.isEmpty()){ removeRestrictionsWithWay(Level.WARNING, way, "ends on tile boundary, restriction is ignored"); } clippedWays = new ArrayList<>(); for (List<Coord> lco : lineSegs) { Way nWay = new Way(way.getId()); nWay.copyTags(way); for(Coord co : lco) { nWay.addPoint(co); if(co.getOnBoundary()) { // this point lies on a boundary // make sure it becomes a node co.incHighwayCount(); } } clippedWays.add(nWay); } } } if(clippedWays != null) { for(Way clippedWay : clippedWays) { addRoadAfterSplittingLoops(new ConvertedWay(cw, clippedWay)); } } else { // no bounding box or way was not clipped addRoadAfterSplittingLoops(cw); } } private void addRoadAfterSplittingLoops(ConvertedWay cw) { Way way = cw.getWay(); // make sure the way has nodes at each end way.getPoints().get(0).incHighwayCount(); way.getPoints().get(way.getPoints().size() - 1).incHighwayCount(); // check if the way is a loop or intersects with itself boolean wayWasSplit = true; // aka rescan required while(wayWasSplit) { List<Coord> wayPoints = way.getPoints(); int numPointsInWay = wayPoints.size(); wayWasSplit = false; // assume way won't be split // check each point in the way to see if it is the same // point as a following point in the way (actually the // same object not just the same coordinates) for(int p1I = 0; !wayWasSplit && p1I < (numPointsInWay - 1); p1I++) { Coord p1 = wayPoints.get(p1I); if (p1.getHighwayCount() < 2) continue; int niceSplitPos = -1; for(int p2I = p1I + 1; !wayWasSplit && p2I < numPointsInWay; p2I++) { Coord p2 = wayPoints.get(p2I); if (p1 != p2){ if (p2.getHighwayCount() > 1) niceSplitPos = p2I; } else { // way is a loop or intersects itself // attempt to split it into two ways // start at point before intersection point // check that splitting there will not produce // a zero length arc - if it does try the // previous point(s) int splitI; if (niceSplitPos >= 0 && safeToSplitWay(wayPoints, niceSplitPos, p1I, p2I)) // prefer to split at a point that is going to be a node anyway splitI = niceSplitPos; else { splitI = p2I - 1; while(splitI > p1I && !safeToSplitWay(wayPoints, splitI, p1I, p2I)) { if (log.isInfoEnabled()) log.info("Looped way", way.getDebugName(), "can't safely split at point[" + splitI + "], trying the preceeding point"); --splitI; } } if(splitI == p1I) { log.warn("Splitting looped way", way.getDebugName(), "would make a zero length arc, so it will have to be pruned at", wayPoints.get(p2I).toOSMURL()); do { log.warn(" Pruning point[" + p2I + "]"); wayPoints.remove(p2I); // next point to inspect has same index --p2I; // but number of points has reduced --numPointsInWay; if (p2I + 1 == numPointsInWay) wayPoints.get(p2I).incHighwayCount(); // if wayPoints[p2I] is the last point // in the way and it is so close to p1 // that a short arc would be produced, // loop back and prune it } while(p2I > p1I && (p2I + 1) == numPointsInWay && p1.equals(wayPoints.get(p2I))); } else { // split the way before the second point if (log.isInfoEnabled()) log.info("Splitting looped way", way.getDebugName(), "at", wayPoints.get(splitI).toOSMURL(), "- it has", (numPointsInWay - splitI - 1 ), "following segment(s)."); Way loopTail = splitWayAt(way, splitI); ConvertedWay next = new ConvertedWay(cw, loopTail); // recursively check (shortened) head for // more loops addRoadAfterSplittingLoops(cw); // now process the tail of the way cw = next; way = loopTail; wayWasSplit = true; } } } } if(!wayWasSplit) { // no split required so make road from way addRoadWithoutLoops(cw); } } } /** * safeToSplitWay() returns true if it is safe (no short arcs will be * created) to split a way at a given position - assumes that the * floor and ceiling points will become nodes even if they are not * yet. * @param points the way's points * @param pos the position we are testing * @param floor lower limit of points to test (inclusive) * @param ceiling upper limit of points to test (inclusive) * @return true if is OK to split as pos */ private static boolean safeToSplitWay(List<Coord> points, int pos, int floor, int ceiling) { Coord candidate = points.get(pos); // avoid running off the ends of the list if(floor < 0) floor = 0; if(ceiling >= points.size()) ceiling = points.size() - 1; // test points after pos for(int i = pos + 1; i <= ceiling; ++i) { Coord p = points.get(i); if(i == ceiling || p.getHighwayCount() > 1) { // point is going to be a node if(candidate.equals(p)) { // coordinates are equal, that's too close return false; } // no need to test further break; } } // test points before pos for(int i = pos - 1; i >= floor; --i) { Coord p = points.get(i); if(i == floor || p.getHighwayCount() > 1) { // point is going to be a node if(candidate.equals(p)) { // coordinates are equal, that's too close return false; } // no need to test further break; } } return true; } private void addRoadWithoutLoops(ConvertedWay cw) { Way way = cw.getWay(); GType gt = cw.getGType(); List<Integer> nodeIndices = new ArrayList<>(); List<Coord> points = way.getPoints(); if (points.size() < 2){ log.warn("road has < 2 points",way.getId(),"(discarding)"); return; } Way trailingWay = null; String debugWayName = way.getDebugName(); // collect the Way's nodes and also split the way if any // inter-node arc length becomes excessive double arcLength = 0; // track the dimensions of the way's bbox so that we can // detect if it would be split by the LineSizeSplitterFilter class WayBBox { int minLat = Integer.MAX_VALUE; int maxLat = Integer.MIN_VALUE; int minLon = Integer.MAX_VALUE; int maxLon = Integer.MIN_VALUE; void addPoint(Coord co) { int lat = co.getLatitude(); if(lat < minLat) minLat = lat; if(lat > maxLat) maxLat = lat; int lon = co.getLongitude(); if(lon < minLon) minLon = lon; if(lon > maxLon) maxLon = lon; } boolean tooBig() { return LineSizeSplitterFilter.testDims(maxLat - minLat, maxLon - minLon) >= 1.0; } } WayBBox wayBBox = new WayBBox(); for(int i = 0; i < points.size(); ++i) { Coord p = points.get(i); wayBBox.addPoint(p); // check if we should split the way at this point to limit // the arc length between nodes if((i + 1) < points.size()) { Coord nextP = points.get(i + 1); double d = p.distance(nextP); for (;;){ int dlat = Math.abs(nextP.getLatitude() - p.getLatitude()); int dlon = Math.abs(nextP.getLongitude() - p.getLongitude()); if (d > MAX_ARC_LENGTH || Math.max(dlat, dlon) >= LineSizeSplitterFilter.MAX_SIZE){ double frac = Math.min(0.5, 0.95 * (MAX_ARC_LENGTH / d)); nextP = p.makeBetweenPoint(nextP, frac); nextP.incHighwayCount(); points.add(i + 1, nextP); double newD = p.distance(nextP); if (log.isInfoEnabled()) log.info("Way", debugWayName, "contains a segment that is", (int)d + "m long but I am adding a new point to reduce its length to", (int)newD + "m"); d = newD; } else break; } wayBBox.addPoint(nextP); if((arcLength + d) > MAX_ARC_LENGTH) { if (i <= 0) log.error("internal error: long arc segment was not split", debugWayName); assert i > 0 : "long arc segment was not split"; assert trailingWay == null : "trailingWay not null #1"; trailingWay = splitWayAt(way, i); // this will have truncated the current Way's // points so the loop will now terminate if (log.isInfoEnabled()) log.info("Splitting way", debugWayName, "at", points.get(i).toOSMURL(), "to limit arc length to", (long)arcLength + "m"); } else if(wayBBox.tooBig()) { if (i <= 0) log.error("internal error: arc segment with big bbox not split", debugWayName); assert i > 0 : "arc segment with big bbox not split"; assert trailingWay == null : "trailingWay not null #2"; trailingWay = splitWayAt(way, i); // this will have truncated the current Way's // points so the loop will now terminate if (log.isInfoEnabled()) log.info("Splitting way", debugWayName, "at", points.get(i).toOSMURL(), "to limit the size of its bounding box"); } else { if(p.getHighwayCount() > 1) { // point is a node so zero arc length arcLength = 0; } arcLength += d; } } if(p.getHighwayCount() > 1) { // this point is a node connecting highways CoordNode coordNode = nodeIdMap.get(p); if(coordNode == null) { // assign a node id coordNode = new CoordNode(p, nextNodeId++, p.getOnBoundary()); nodeIdMap.put(p, coordNode); } if (p instanceof CoordPOI){ // check if this poi should be converted to a route restriction CoordPOI cp = (CoordPOI) p; if (cp.getConvertToViaInRouteRestriction()){ String wayPOI = way.getTag(WAY_POI_NODE_IDS); if (wayPOI != null && wayPOI.contains("[" + cp.getNode().getId() + "]")){ byte nodeAccess = AccessTagsAndBits.evalAccessTags(cp.getNode()); if (nodeAccess != cw.getAccess()){ List<Way> wayList = poiRestrictions.get(cp.getNode()); if (wayList == null){ wayList = new ArrayList<>(); poiRestrictions.put(cp.getNode(), wayList); } wayList.add(way); } } } } // add this index to node Indexes (should not already be there) assert !nodeIndices.contains(i) : debugWayName + " has multiple nodes for point " + i + " new node is " + p.toOSMURL(); nodeIndices.add(i); if((i + 1) < points.size() && nodeIndices.size() == MAX_NODES_IN_WAY) { // this isn't the last point in the way so split // it here to avoid exceeding the max nodes in way // limit assert trailingWay == null : "trailingWay not null #7"; trailingWay = splitWayAt(way, i); // this will have truncated the current Way's // points so the loop will now terminate if (log.isInfoEnabled()) log.info("Splitting way", debugWayName, "at", points.get(i).toOSMURL(), "as it has at least", MAX_NODES_IN_WAY, "nodes"); } } } MapLine line = new MapLine(); elementSetup(line, cw.getGType(), way); line.setPoints(points); MapRoad road = new MapRoad(nextRoadId++, way.getId(), line); if (routable == false) road.skipAddToNOD(true); boolean doFlareCheck = true; if (cw.isRoundabout()){ road.setRoundabout(true); doFlareCheck = false; } if(way.tagIsLikeYes("mkgmap:synthesised")) { road.setSynthesised(true); doFlareCheck = false; } if(way.tagIsLikeNo("mkgmap:flare-check")) { doFlareCheck = false; } else if(way.tagIsLikeYes("mkgmap:flare-check")) { doFlareCheck = true; } road.doFlareCheck(doFlareCheck); road.setLinkRoad(gt.getType() == 0x08 || gt.getType() == 0x09); // set road parameters // copy road class and road speed road.setRoadClass(cw.getRoadClass()); road.setSpeed(cw.getRoadSpeed()); if (cw.isOneway()) { road.setDirection(true); road.setOneway(); } road.setAccess(cw.getAccess()); // does the road have a carpool lane? if (cw.isCarpool()) road.setCarpoolLane(); if (cw.isThroughroute() == false) road.setNoThroughRouting(); if(cw.isToll()) road.setToll(); // by default, ways are paved if(cw.isUnpaved()) road.paved(false); // by default, way's are not ferry routes if(cw.isFerry()) road.ferry(true); int numNodes = nodeIndices.size(); if (way.isViaWay() && numNodes > 2){ List<RestrictionRelation> rrList = wayRelMap.get(way.getId()); for (RestrictionRelation rr : rrList){ rr.updateViaWay(way, nodeIndices); } } if(numNodes > 0) { // replace Coords that are nodes with CoordNodes for(int i = 0; i < numNodes; ++i) { int n = nodeIndices.get(i); Coord coord = points.get(n); CoordNode thisCoordNode = nodeIdMap.get(coord); assert thisCoordNode != null : "Way " + debugWayName + " node " + i + " (point index " + n + ") at " + coord.toOSMURL() + " yields a null coord node"; boolean boundary = coord.getOnBoundary(); if(boundary && log.isInfoEnabled()) { log.info("Way", debugWayName + "'s point #" + n, "at", coord.toOSMURL(), "is a boundary node"); } points.set(n, thisCoordNode); } } if (roadLog.isInfoEnabled()) { // shift the bits so that they have the correct position int cmpAccess = (road.getRoadDef().getTabAAccess() & 0xff) + ((road.getRoadDef().getTabAAccess() & 0xc000) >> 6); if (road.isDirection()) { cmpAccess |= 1<<10; } String access = String.format("%11s",Integer.toBinaryString(cmpAccess)).replace(' ', '0'); roadLog.info(String.format("%19d 0x%-2x %11s %6d %s", way.getId(), road.getType(), access, road.getPoints().size(),Arrays.toString(road.getLabels()))); } // add the road to the housenumber generator // it will add the road later on to the lineAdder housenumberGenerator.addRoad(way, road); if(trailingWay != null) addRoadWithoutLoops(new ConvertedWay(cw, trailingWay)); } /** * Check if the first or last of the coords of the way has the fixme flag set * @param way the way to check * @return true if fixme flag was found */ private static boolean checkFixmeCoords(Way way) { if (way.getPoints().get(0).isFixme()) return true; if (way.getPoints().get(way.getPoints().size()-1).isFixme()) return true; return false; } /** * split a Way at the specified point and return the new Way (the * original Way is truncated, both ways will contain the split point) * @param way the way to split * @param index the split position. * @return the trailing part of the way */ private static Way splitWayAt(Way way, int index) { if (way.isViaWay()){ log.warn("via way of restriction is split, restriction will be ignored",way); } Way trailingWay = new Way(way.getId()); List<Coord> wayPoints = way.getPoints(); int numPointsInWay = wayPoints.size(); for(int i = index; i < numPointsInWay; ++i) trailingWay.addPoint(wayPoints.get(i)); // ensure split point becomes a node wayPoints.get(index).incHighwayCount(); // copy the way's name and tags to the new way trailingWay.copyTags(way); // remove the points after the split from the original way // it's probably more efficient to remove from the end first for(int i = numPointsInWay - 1; i > index; --i) wayPoints.remove(i); return trailingWay; } /** * Increment the highway counter for each coord of each road. * As a result, all road junctions have a count > 1. */ private void setHighwayCounts(){ log.info("Maintaining highway counters"); long lastId = 0; List<Way> dupIdHighways = new ArrayList<>(); for (ConvertedWay cw :roads){ if (!cw.isValid()) continue; Way way = cw.getWay(); if (way.getId() == lastId) { log.debug("Road with identical id:", way.getId()); dupIdHighways.add(way); continue; } lastId = way.getId(); List<Coord> points = way.getPoints(); for (Coord p:points){ p.incHighwayCount(); } } // go through all duplicated highways and increase the highway counter of all crossroads for (Way way : dupIdHighways) { List<Coord> points = way.getPoints(); // increase the highway counter of the first and last point points.get(0).incHighwayCount(); points.get(points.size()-1).incHighwayCount(); // for all other points increase the counter only if other roads are connected for (int i = 1; i < points.size()-1; i++) { Coord p = points.get(i); if (p.getHighwayCount() > 1) { // this is a crossroads - mark that the duplicated way is also part of it p.incHighwayCount(); } } } } /** * Increment the highway counter for each coord of each road. * As a result, all road junctions have a count > 1. */ private void resetHighwayCounts(){ log.info("Resetting highway counters"); long lastId = 0; for (ConvertedWay cw :roads){ if (!cw.isValid()) continue; Way way = cw.getWay(); if (way.getId() == lastId) { continue; } lastId = way.getId(); List<Coord> points = way.getPoints(); for (Coord p:points){ p.resetHighwayCount(); } } } /** * Detect roads that do not share any node with another road. * If such a road has the mkgmap:set_unconnected_type tag, add it as line, not as a road. */ private void findUnconnectedRoads(){ Map<Coord, HashSet<Way>> connectors = new IdentityHashMap<>(roads.size()*2); // for dead-end-check only: will contain ways with loops (also simply closed ways) HashSet<Way> selfConnectors = new HashSet<>(); // collect nodes that might connect roads long lastId = 0; for (ConvertedWay cw :roads){ Way way = cw.getWay(); if (way.getId() == lastId) continue; lastId = way.getId(); for (Coord p:way.getPoints()){ if (p.getHighwayCount() > 1){ HashSet<Way> ways = connectors.get(p); if (ways == null){ ways = new HashSet<>(); connectors.put(p, ways); } boolean wasNew = ways.add(way); if (!wasNew && reportDeadEnds > 0) selfConnectors.add(way); } } } // find roads that are not connected // count downwards because we are removing elements Iterator<ConvertedWay> iter = roads.iterator(); while(iter.hasNext()){ ConvertedWay cw = iter.next(); if (!cw.isValid()) continue; Way way = cw.getWay(); if(reportDeadEnds > 0){ // report dead ends of oneway roads if (cw.isOneway() && !way.tagIsLikeNo("mkgmap:dead-end-check")) { List<Coord> points = way.getPoints(); int[] pointsToCheck = {0, points.size()-1}; if (points.get(pointsToCheck[0]) == points.get(pointsToCheck[1])) continue; // skip closed way for (int pos: pointsToCheck ){ boolean isDeadEnd = true; boolean isDeadEndOfMultipleWays = true; Coord p = points.get(pos); if (bbox.contains(p) == false || p.getOnBoundary()) isDeadEnd = false; // we don't know enough about possible connections else if (p.getHighwayCount() < 2){ isDeadEndOfMultipleWays = false; } else { HashSet<Way> ways = connectors.get(p); if (ways.size() <= 1) isDeadEndOfMultipleWays = false; for (Way connectedWay: ways){ if (!isDeadEnd) break; if (way == connectedWay){ if (selfConnectors.contains(way)){ // this might be a P-shaped oneway, // check if it has other exists in the loop part if (pos == 0){ for (int k = pos+1; k < points.size()-1; k++){ Coord pTest = points.get(k); if (pTest == p) break; // found no other exit if (pTest.getHighwayCount() > 1){ isDeadEnd = false; break; } } }else { for (int k = pos-1; k >= 0; k--){ Coord pTest = points.get(k); if (pTest == p) break; // found no other exit if (pTest.getHighwayCount() > 1){ isDeadEnd = false; break; } } } } continue; } List<Coord> otherPoints = connectedWay.getPoints(); Coord otherFirst = otherPoints.get(0); Coord otherLast = otherPoints.get(otherPoints.size()-1); if (otherFirst == otherLast || connectedWay.tagIsLikeYes(onewayTagKey) == false) isDeadEnd = false; else { Coord pOther; if (pos != 0) pOther = otherLast; else pOther = otherFirst; if (p != pOther){ // way is connected to a point on a oneway which allows going on isDeadEnd = false; } } } } if (isDeadEnd && (isDeadEndOfMultipleWays || reportDeadEnds > 1)){ log.warn("Oneway road " + way.getId() + " with tags " + way.toTagString() + ((pos==0) ? " comes from":" goes to") + " nowhere at " + p.toOSMURL()); } } } } String replType = way.getTag("mkgmap:set_unconnected_type"); if (replType != null){ boolean isConnected = false; boolean onBoundary = false; for (Coord p:way.getPoints()){ if (p.getOnBoundary()) onBoundary = true; if (p.getHighwayCount() > 1){ HashSet<Way> ways = connectors.get(p); if (ways != null && ways.size() > 1){ isConnected = true; break; } } } if (!isConnected){ if (onBoundary){ log.info("road not connected to other roads but is on boundary:", way.toBrowseURL()); } else { if ("none".equals(replType)) log.info("road not connected to other roads, is ignored:", way.toBrowseURL()); else { int typeNoConnection = -1; try{ typeNoConnection = Integer.decode(replType); if (GType.isRoutableLineType(typeNoConnection)){ typeNoConnection = -1; log.error("type value in mkgmap:set_unconnected_type should not be a routable type:" + replType); } } catch (NumberFormatException e){ log.warn("invalid type value in mkgmap:set_unconnected_type:", replType); } if (typeNoConnection != -1 ){ log.info("road not connected to other roads, added as line with type", replType + ":", way.toBrowseURL()); addLine(way, cw.getGType(), typeNoConnection); } else { log.warn("road not connected to other roads, but replacement type is invalid. Dropped:", way.toBrowseURL()); } } iter.remove(); } } } } } /** * Make sure that only CoordPOI which affect routing will be treated as * nodes in the following routines. */ private void filterCoordPOI() { if (!linkPOIsToWays) return; log.info("translating CoordPOI"); for (ConvertedWay cw: roads) { if (!cw.isValid()) continue; Way way = cw.getWay(); if ("true".equals(way.getTag("mkgmap:way-has-pois"))) { String wayPOI = ""; List<Coord> points = way.getPoints(); int numPoints = points.size(); for (int i = 0;i < numPoints; i++) { Coord p = points.get(i); if (p instanceof CoordPOI){ CoordPOI cp = (CoordPOI) p; Node node = cp.getNode(); boolean usedInThisWay = false; byte wayAccess = cw.getAccess(); if (node.getTag("mkgmap:road-class") != null || node.getTag("mkgmap:road-speed") != null ) { if (wayAccess != AccessTagsAndBits.FOOT) usedInThisWay = true; } byte nodeAccess = AccessTagsAndBits.evalAccessTags(node); if(nodeAccess != (byte)0xff){ // barriers etc. if ((wayAccess & nodeAccess) != wayAccess){ // node is more restrictive if (p.getHighwayCount() >= 2 || (i != 0 && i != numPoints-1)){ usedInThisWay = true; cp.setConvertToViaInRouteRestriction(true); } else { log.info("POI node", node.getId(), "with access restriction is ignored, it is not connected to other routable ways"); } } else log.info("Access restriction in POI node", node.toBrowseURL(), "was ignored for way", way.toBrowseURL()); } if (usedInThisWay){ cp.setUsed(true); wayPOI += "["+ node.getId()+"]"; } } } if (wayPOI.isEmpty()) { way.deleteTag("mkgmap:way-has-pois"); log.info("ignoring CoordPOI(s) for way", way.toBrowseURL(), "because routing is not affected."); } else { way.addTag(WAY_POI_NODE_IDS, wayPOI); } } } } @Override public Boolean getDriveOnLeft(){ assert roads == null : "getDriveOnLeft() should be called after end()"; return driveOnLeft; } }