/*
* 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.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 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 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 final boolean checkRoundabouts
;
private int reportDeadEnds
;
private final boolean linkPOIsToWays
;
private final boolean mergeRoads
;
private WrongAngleFixer wrongAngleFixer
;
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
;
wayRules = style.
getWayRules();
nodeRules = style.
getNodeRules();
lineRules = style.
getLineRules();
polygonRules = style.
getPolygonRules();
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
);
}
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;
wrongAngleFixer =
new WrongAngleFixer
(bbox
);
}
/** 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
);
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");
}
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++
;
String country = way.
getTag(countryTagKey
);
if (country
!=
null) {
if (LocatorConfig.
get().
getDriveOnLeftFlag(country
))
numDriveOnLeftRoads++
;
else
numDriveOnRightRoads++
;
} 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
);
String name = way.
getTag("name");
if(name
!=
null)
name +=
" (cycleway)";
else
name =
"cycleway";
cycleWay.
addTag("name", name
);
cycleWay.
addTag("access",
"no");
cycleWay.
addTag("bicycle",
"yes");
cycleWay.
addTag("foot",
"no");
cycleWay.
addTag("mkgmap:synthesised",
"yes");
cycleWay.
addTag(onewayTagKey,
"no");
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
() {
style.
reportStats();
driveOnLeft = calcDrivingSide
();
setHighwayCounts
();
findUnconnectedRoads
();
rotateClosedWaysToFirstNode
();
filterCoordPOI
();
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
);
createRouteRestrictionsFromPOI
();
poiRestrictions =
null;
for (RestrictionRelation rr : restrictions
) {
rr.
addRestriction(collector, nodeIdMap
);
}
roads =
null;
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());
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
();
log.
warn("Motorway exit", node.
getName(),
"(" + node.
toBrowseURL() +
") has no motorway! (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());
collector.
addPoint(mp
);
}
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 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();
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
);
// get arc size as a proportion of the max allowed - a
// value greater than 1.0 indicate that the bbox is
// too large in at least one dimension
double arcProp = LineSizeSplitterFilter.
testDims(nextP.
getLatitude() -
p.
getLatitude(),
nextP.
getLongitude() -
p.
getLongitude());
if(arcProp
>=
1.0 || d
> MAX_ARC_LENGTH
) {
nextP = p.
makeBetweenPoint(nextP,
0.95 * Math.
min(1 / arcProp, MAX_ARC_LENGTH / d
));
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
;
}
wayBBox.
addPoint(nextP
);
if((arcLength + d
) > MAX_ARC_LENGTH
) {
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()) {
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
(way.
getId(), line
);
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
);
}
}
road.
setNumNodes(numNodes
);
if(numNodes
> 0) {
// replace Coords that are nodes with CoordNodes
boolean hasInternalNodes =
false;
for(int i =
0; i
< numNodes
; ++i
) {
int n = nodeIndices.
get(i
);
if(n
> 0 && n
< points.
size() -
1)
hasInternalNodes =
true;
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
);
}
road.
setStartsWithNode(nodeIndices.
get(0) ==
0);
road.
setInternalNodes(hasInternalNodes
);
}
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
);
}
}
}
}
public Boolean getDriveOnLeft
(){
assert roads ==
null :
"getDriveOnLeft() should be called after end()";
return driveOnLeft
;
}
}