/*
* 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 java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Level;
import java.util.regex.Pattern;
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.NODHeader;
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.LocatorUtil;
import uk.me.parabola.mkgmap.filters.LineSizeSplitterFilter;
import uk.me.parabola.mkgmap.filters.LineSplitterFilter;
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.general.RoadNetwork;
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.TypeResult;
import uk.me.parabola.mkgmap.reader.osm.Way;
/**
* 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 final List<String> nameTagList
;
private final MapCollector collector
;
private Clipper clipper = Clipper.
NULL_CLIPPER;
private Area bbox
;
// restrictions associates lists of turn restrictions with the
// Coord corresponding to the restrictions' 'via' node
private final Map<Coord,
List<RestrictionRelation
>> restrictions =
new IdentityHashMap<Coord,
List<RestrictionRelation
>>();
private final List<Relation> throughRouteRelations =
new ArrayList<Relation>();
// 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 =
75000;
private static final int MAX_POINTS_IN_WAY = LineSplitterFilter.
MAX_POINTS_IN_LINE;
private static final int MAX_POINTS_IN_ARC = MAX_POINTS_IN_WAY
;
private static final int MAX_NODES_IN_WAY =
64; // possibly could be increased
private static final double MIN_DISTANCE_BETWEEN_NODES =
5.5;
// nodeIdMap maps a Coord into a nodeId
private IdentityHashMap<Coord,
Integer> nodeIdMap =
new IdentityHashMap<Coord,
Integer>();
private List<Way
> roads =
new ArrayList<Way
>();
private List<GType
> roadTypes =
new ArrayList<GType
>();
private List<Way
> lines =
new ArrayList<Way
>();
private List<GType
> lineTypes =
new ArrayList<GType
>();
HashMap<Long, Way
> modifiedRoads =
new HashMap<Long, Way
>();
HashSet<Long> deletedRoads =
new HashSet<Long>();
private double minimumArcLength
;
private int nextNodeId =
1;
private HousenumberGenerator housenumberGenerator
;
private final Rule wayRules
;
private final Rule nodeRules
;
private final Rule lineRules
;
private final Rule polygonRules
;
private final boolean ignoreMaxspeeds
;
private boolean driveOnLeft
;
private boolean driveOnRight
;
private final boolean checkRoundabouts
;
private static final Pattern ENDS_IN_MPH_PATTERN =
Pattern.
compile(".*mph");
private static final Pattern REMOVE_MPH_PATTERN =
Pattern.
compile("[ \t]*mph");
private static final Pattern REMOVE_KPH_PATTERN =
Pattern.
compile("[ \t]*kmh");
private static final Pattern SEMI_PATTERN =
Pattern.
compile(";");
class AccessMapping
{
private final String type
;
private final int index
;
AccessMapping
(String type,
int index
) {
this.
type = type
;
this.
index = index
;
}
}
private final AccessMapping
[] accessMap =
{
new AccessMapping
("access", RoadNetwork.
NO_MAX),
// must be first in list
new AccessMapping
("bicycle", RoadNetwork.
NO_BIKE),
new AccessMapping
("carpool", RoadNetwork.
NO_CARPOOL),
new AccessMapping
("foot", RoadNetwork.
NO_FOOT),
new AccessMapping
("hgv", RoadNetwork.
NO_TRUCK),
new AccessMapping
("motorcar", RoadNetwork.
NO_CAR),
new AccessMapping
("motorcycle", RoadNetwork.
NO_CAR),
new AccessMapping
("psv", RoadNetwork.
NO_BUS),
new AccessMapping
("taxi", RoadNetwork.
NO_TAXI),
new AccessMapping
("emergency", RoadNetwork.
NO_EMERGENCY),
new AccessMapping
("delivery", RoadNetwork.
NO_DELIVERY),
new AccessMapping
("goods", RoadNetwork.
NO_DELIVERY),
};
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,
Properties props
) {
this.
collector = collector
;
nameTagList = LocatorUtil.
getNameTags(props
);
wayRules = style.
getWayRules();
nodeRules = style.
getNodeRules();
lineRules = style.
getLineRules();
polygonRules = style.
getPolygonRules();
housenumberGenerator =
new HousenumberGenerator
(props
);
ignoreMaxspeeds = props.
getProperty("ignore-maxspeeds") !=
null;
driveOnLeft = props.
getProperty("drive-on-left") !=
null;
NODHeader.
setDriveOnLeft(driveOnLeft
);
driveOnRight = props.
getProperty("drive-on-right") !=
null;
checkRoundabouts = props.
getProperty("check-roundabouts") !=
null;
LineAdder overlayAdder = style.
getOverlays(lineAdder
);
if (overlayAdder
!=
null)
lineAdder = overlayAdder
;
String rsa = props.
getProperty("remove-short-arcs",
"5");
minimumArcLength =
(!rsa.
isEmpty())? Double.
parseDouble(rsa
) :
0.0;
}
/**
* 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.
*/
public void convertWay
(final Way way
) {
if (way.
getPoints().
size() < 2)
return;
if (way.
getTagCount() ==
0) {
// no tags => nothing to convert
return;
}
preConvertRules
(way
);
housenumberGenerator.
addWay(way
);
Rule rules
;
if ("polyline".
equals(way.
getTag("mkgmap:stylefilter")))
rules = lineRules
;
else if ("polygon".
equals(way.
getTag("mkgmap:stylefilter")))
rules = polygonRules
;
else
rules = wayRules
;
rules.
resolveType(way,
new TypeResult
() {
public void add
(Element el, GType type
) {
if (type.
isContinueSearch()) {
// If not already copied, do so now
if (el == way
)
el = way.
copy();
// Not sure if this is needed as this really is a completely new way.
// originalWay.put(el, way);
}
postConvertRules
(el, type
);
addConvertedWay
((Way
) el, type
);
}
});
}
private void addConvertedWay
(Way way, GType foundType
) {
if (foundType.
getFeatureKind() == FeatureKind.
POLYLINE) {
if(foundType.
isRoad() &&
!MapObject.
hasExtendedType(foundType.
getType())){
roads.
add(way
);
roadTypes.
add(new GType
(foundType
));
}
else {
lines.
add(way
);
lineTypes.
add(new GType
(foundType
));
}
}
else
addShape
(way, foundType
);
}
/**
* 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
);
housenumberGenerator.
addNode(node
);
nodeRules.
resolveType(node,
new TypeResult
() {
public void add
(Element el, GType type
) {
if (type.
isContinueSearch()) {
// If not already copied, do so now
if (el == node
)
el = node.
copy();
// Not sure if this is needed as this really is a completely new way.
// originalWay.put(el, way);
}
postConvertRules
(el, type
);
addPoint
((Node) el, type
);
}
});
}
/**
* Rules to run before converting the element.
*/
private void preConvertRules
(Element el
) {
if (nameTagList ==
null)
return;
for (String t : nameTagList
) {
String val = el.
getTag(t
);
if (val
!=
null) {
el.
addTag("name", val
);
break;
}
}
}
/**
* Built in rules to run after converting the element.
*/
private void postConvertRules
(Element el, GType type
) {
// Set the name from the 'name' tag or failing that from
// the default_name.
el.
setName(el.
getTag("name"));
if (el.
getName() ==
null)
el.
setName(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()));
}
public void end
() {
setHighwayCounts
();
findUnconnectedRoads
();
removeShortArcsByMergingNodes
(minimumArcLength
);
// make sure that copies of modified roads are have equal points
for (int i =
0; i
< lines.
size(); i++
){
Way line = lines.
get(i
);
if (deletedRoads.
contains(line.
getId())){
lines.
set(i,
null);
continue;
}
Way modWay = modifiedRoads.
get(line.
getId());
if (modWay
!=
null){
List<Coord
> points = line.
getPoints();
points.
clear();
points.
addAll(modWay.
getPoints());
}
}
deletedRoads =
null;
modifiedRoads =
null;
for (int i =
0; i
< lines.
size(); i++
){
Way line = lines.
get(i
);
if (line ==
null)
continue;
GType gt = lineTypes.
get(i
);
addLine
(line, gt
);
}
lines =
null;
lineTypes =
null;
// add the roads after the other lines
for (int i =
0; i
< roads.
size(); i++
){
Way road = roads.
get(i
);
if (road ==
null)
continue;
GType gt = roadTypes.
get(i
);
addRoad
(road, gt
);
}
roads =
null;
roadTypes =
null;
housenumberGenerator.
generate(lineAdder
);
Collection<List<RestrictionRelation
>> lists = restrictions.
values();
for (List<RestrictionRelation
> l : lists
) {
for (RestrictionRelation rr : l
) {
rr.
addRestriction(collector
);
}
}
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");
}
}
Integer nodeId =
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;
}
nodeId = nodeIdMap.
get(junctionPoint
);
if(nodeId ==
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(nodeId
!=
null && w1
!=
null && w2
!=
null)
collector.
addThroughRoute(nodeId, w1.
getId(), w2.
getId());
}
// return memory to GC
nodeIdMap =
null;
throughRouteRelations.
clear();
restrictions.
clear();
}
/**
* 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()) {
List<RestrictionRelation
> lrr = restrictions.
get(rr.
getViaCoord());
if(lrr ==
null) {
lrr =
new ArrayList<RestrictionRelation
>();
restrictions.
put(rr.
getViaCoord(), lrr
);
}
lrr.
add(rr
);
}
}
else if("through_route".
equals(relation.
getTag("type"))) {
throughRouteRelations.
add(relation
);
}
}
private void addLine
(Way way, GType gt
) {
List<Coord
> wayPoints = way.
getPoints();
List<Coord
> points =
new ArrayList<Coord
>(wayPoints.
size());
double lineLength =
0;
Coord lastP =
null;
for(Coord p : wayPoints
) {
if (lastP
!=
null && p.
equals(lastP
) && p
instanceof CoordPOI ==
false && p
instanceof CoordNode ==
false)
continue;
points.
add(p
);
if(lastP
!=
null) {
lineLength += p.
distance(lastP
);
if(lineLength
>= MAX_LINE_LENGTH
) {
log.
info("Splitting line " + way.
toBrowseURL() +
" at " + p.
toOSMURL() +
" to limit its length to " +
(long)lineLength +
"m");
addLine
(way, gt, points
);
points =
new ArrayList<Coord
>(wayPoints.
size() - points.
size() +
1);
points.
add(p
);
lineLength =
0;
}
}
lastP = p
;
}
if(points.
size() > 1)
addLine
(way, gt, points
);
}
private void addLine
(Way way, GType gt,
List<Coord
> points
) {
MapLine line =
new MapLine
();
elementSetup
(line, gt, way
);
line.
setPoints(points
);
if (way.
isBoolTag("oneway"))
line.
setDirection(true);
if (way.
isBoolTag("mkgmap:skipSizeFilter"))
line.
setSkipSizeFilter(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.
//
// Here isClosed means that it is really closed in OSM, and therefore it is safe to clip the line
// segment to the tile boundaries.
if (!way.
isClosed())
return;
final MapShape shape =
new MapShape
();
elementSetup
(shape, gt, way
);
shape.
setPoints(way.
getPoints());
if (way.
isBoolTag("mkgmap:skipSizeFilter"))
shape.
setSkipSizeFilter(true);
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.
getLocation().
toOSMURL() +
") 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 String combineRefs
(Element element
) {
String ref =
Label.
squashSpaces(element.
getTag("ref"));
String int_ref =
Label.
squashSpaces(element.
getTag("int_ref"));
if(int_ref
!=
null) {
if(ref ==
null)
ref = int_ref
;
else
ref +=
";" + int_ref
;
}
String nat_ref =
Label.
squashSpaces(element.
getTag("nat_ref"));
if(nat_ref
!=
null) {
if(ref ==
null)
ref = nat_ref
;
else
ref +=
";" + nat_ref
;
}
String reg_ref =
Label.
squashSpaces(element.
getTag("reg_ref"));
if(reg_ref
!=
null) {
if(ref ==
null)
ref = reg_ref
;
else
ref +=
";" + reg_ref
;
}
return ref
;
}
private boolean displayNameWarning =
true;
private void elementSetup
(MapElement ms, GType gt,
Element element
) {
String name =
Label.
squashSpaces(element.
getName());
String refs = combineRefs
(element
);
// Insert mkgmap:display_name as first ref.
// This causes mkgmap:display_name to be displayed in routing
// directions, instead of only the ref.
String displayName =
Label.
squashSpaces(element.
getTag("mkgmap:display_name"));
// be downward compatible if old tag display_name is used
if (displayName ==
null) {
// get the old tag display_name which should not be used any more (Dec 2012)
displayName =
Label.
squashSpaces(element.
getTag("display_name"));
if (displayName
!=
null && displayNameWarning
) {
System.
err.
println("WARNING: Style uses tag 'display_name' which is deprecated " +
"and will be removed soon. Please use the new tag 'mkgmap:display_name' instead.");
log.
warn("Style uses tag 'display_name' which is deprecated",
"and will be removed soon. Please use the new tag 'mkgmap:display_name' instead.");
displayNameWarning =
false;
}
}
if (displayName
!=
null) {
// substitute '/' for ';' in mkgmap:display_name to avoid it
// getting split below
displayName = displayName.
replace(";",
"/");
if (refs ==
null)
refs = displayName
;
else
refs = displayName +
";" + refs
;
}
if(name ==
null && refs
!=
null) {
// use first ref as name
String[] names = SEMI_PATTERN.
split(refs
);
if (names.
length > 0)
name = names
[0].
trim();
}
else if(name
!=
null) {
// remove leading spaces (don't use trim() to avoid zapping
// shield codes)
char leadingCode =
0;
if(name.
length() > 1 &&
name.
charAt(0) < 0x20
&&
name.
charAt(1) ==
' ') {
leadingCode = name.
charAt(0);
name = name.
substring(2);
}
while(!name.
isEmpty() && name.
charAt(0) ==
' ')
name = name.
substring(1);
if(leadingCode
!=
0)
name = leadingCode + name
;
}
if(name
!=
null)
ms.
setName(name
);
if(refs
!=
null)
ms.
setRef(refs
);
ms.
setType(gt.
getType());
ms.
setMinResolution(gt.
getMinResolution());
ms.
setMaxResolution(gt.
getMaxResolution());
// Now try to get some address info for POIs
String country = element.
getTag("mkgmap:country");
String region = element.
getTag("mkgmap:region");
String city = element.
getTag("mkgmap:city");
String zip = element.
getTag("mkgmap:postal_code");
String street = element.
getTag("mkgmap:street");
String houseNumber = element.
getTag("mkgmap:housenumber");
String phone = element.
getTag("mkgmap:phone");
String isIn = element.
getTag("mkgmap:is_in");
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()));
}
}
void addRoad
(Way way, GType gt
) {
String oneWay = way.
getTag("oneway");
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();
way.
addTag("oneway",
"yes");
if("roundabout".
equals(way.
getTag("junction")))
log.
warn("Roundabout " + way.
getId() +
" has reverse oneway tag (" + way.
getPoints().
get(0).
toOSMURL() +
")");
}
if("roundabout".
equals(way.
getTag("junction"))) {
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
&&
way.
getPoints().
size() > 2 &&
!way.
isBoolTag("mkgmap:no-dir-check") &&
!way.
isNotBoolTag("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);
// don't check segments that are very short
if(pi.
quickDistance(centre
) > 2.5 &&
pi.
quickDistance(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
&& !driveOnRight
) {
if (clockwise
) {
log.
info("Roundabout " + way.
getId() +
" is clockwise so assuming vehicles should drive on left side of road (" + centre.
toOSMURL() +
")");
driveOnLeft =
true;
NODHeader.
setDriveOnLeft(true);
} else {
log.
info("Roundabout " + way.
getId() +
" is anti-clockwise so assuming vehicles should drive on right side of road (" + centre.
toOSMURL() +
")");
driveOnRight =
true;
}
}
if (driveOnLeft
&& !clockwise ||
driveOnRight
&& clockwise
)
{
log.
warn("Roundabout " + way.
getId() +
" direction is wrong - reversing it (see " + centre.
toOSMURL() +
")");
way.
reverse();
}
} else if (driveOnLeft
&& !clockwise ||
driveOnRight
&& clockwise
)
{
// roundabout is a line
log.
warn("Roundabout segment " + way.
getId() +
" direction looks wrong (see " + points.
get(0).
toOSMURL() +
")");
}
}
}
String frigFactorTag = way.
getTag("mkgmap:frig_roundabout");
if(frigFactorTag
!=
null) {
// do special roundabout frigging to make gps
// routing prompt use the correct exit number
double frigFactor =
0.25; // default
try {
frigFactor =
Double.
parseDouble(frigFactorTag
);
}
catch (NumberFormatException nfe
) {
// relax, tag was probably not a number anyway
}
frigRoundabout
(way, frigFactor
);
}
}
// process any Coords that have a POI associated with them
if("true".
equals(way.
getTag("mkgmap:way-has-pois"))) {
List<Coord
> points = way.
getPoints();
// look for POIs that modify the way's road class or speed
// this could be e.g. highway=traffic_signals that reduces the
// road speed to cause a short increase of traveling time
for(int i =
0; i
< points.
size(); ++i
) {
Coord p = points.
get(i
);
if(p
instanceof CoordPOI
) {
CoordPOI cp =
(CoordPOI
)p
;
Node node = cp.
getNode();
String roadClass = node.
getTag("mkgmap:road-class");
String roadSpeed = node.
getTag("mkgmap:road-speed");
if(roadClass
!=
null || roadSpeed
!=
null) {
// if the way has more than one point
// following this one, split the way at the
// next point to limit the size of the
// affected region
if((i +
2) < points.
size() &&
safeToSplitWay
(points, i +
1, i, points.
size() -
1)) {
Way tail = splitWayAt
(way, i +
1);
// recursively process tail of way
addRoad
(tail, gt
);
}
// we can't modify the road class or type in
// the GType as that's global so for now just
// transfer the tags to the way
if(roadClass
!=
null) {
way.
addTag("mkgmap:road-class", roadClass
);
String val = node.
getTag("mkgmap:road-class-min");
if(val
!=
null)
way.
addTag("mkgmap:road-class-min", val
);
val = node.
getTag("mkgmap:road-class-max");
if(val
!=
null)
way.
addTag("mkgmap:road-class-max", val
);
}
if(roadSpeed
!=
null) {
way.
addTag("mkgmap:road-speed", roadSpeed
);
String val = node.
getTag("mkgmap:road-speed-min");
if(val
!=
null)
way.
addTag("mkgmap:road-speed-min", val
);
val = node.
getTag("mkgmap:road-speed-max");
if(val
!=
null)
way.
addTag("mkgmap:road-speed-max", val
);
}
}
}
// if this isn't the first (or 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
> 0 &&
(i +
1) < points.
size() &&
points.
get(i +
1) instanceof CoordPOI
) {
CoordPOI cp =
(CoordPOI
)points.
get(i +
1);
Node node = cp.
getNode();
if(node.
getTag("mkgmap:road-class") !=
null ||
node.
getTag("mkgmap:road-speed") !=
null) {
if(safeToSplitWay
(points, i, i -
1, points.
size() -
1)) {
Way tail = splitWayAt
(way, i
);
// recursively process tail of way
addRoad
(tail, gt
);
}
}
}
}
// now look for POIs that have the "access" tag defined -
// if they do, copy the access permissions to the way -
// what we want to achieve is modifying the way's access
// permissions where it passes through the POI without
// affecting the rest of the way too much - to that end we
// split the way before and after the POI - if necessary,
// extra points are inserted before and after the POI to
// limit the size of the affected region
final double stubSegmentLength =
25; // metres
for(int i =
0; i
< points.
size(); ++i
) {
Coord p = points.
get(i
);
// check if this POI modifies access and if so, split
// the way at the following point (if any) and then
// copy its access restrictions to the way
if(p
instanceof CoordPOI
) {
CoordPOI cp =
(CoordPOI
)p
;
Node node = cp.
getNode();
if(node.
getTag("access") !=
null) {
// if this or the next point are not the last
// points in the way, split at the next point
// taking care not to produce a short arc
if((i +
1) < points.
size()) {
Coord p1 = points.
get(i +
1);
// check if the next point is further away
// than we would like
double dist = p.
distance(p1
);
if(dist
>=
(2 * stubSegmentLength
)) {
// insert a new point after the POI to
// make a short stub segment
p1 = p.
makeBetweenPoint(p1, stubSegmentLength / dist
);
points.
add(i +
1, p1
);
}
// now split the way at the next point to
// limit the region that has restricted
// access
if(!p.
equals(p1
) &&
((i +
2) == points.
size() ||
!p1.
equals(points.
get(i +
2)))) {
Way tail = splitWayAt
(way, i +
1);
// recursively process tail of way
addRoad
(tail, gt
);
}
}
// make the POI a node so that the region with
// restricted access is split into two as far
// as routing is concerned - this should stop
// routing across the POI when the start point
// is within the restricted region and the
// destination point is outside of the
// restricted region on the other side of the
// POI
// however, this still doesn't stop routing
// across the POI when both the start and end
// points are either side of the POI and both
// are in the restricted region
p.
incHighwayCount();
// copy all of the POI's access restrictions
// to the way segment
for (AccessMapping anAccessMap : accessMap
) {
String accessType = anAccessMap.
type;
String accessModifier = node.
getTag(accessType
);
if(accessModifier
!=
null)
way.
addTag(accessType, accessModifier
);
}
}
}
// check if the next point modifies access and if so,
// split the way either here or at a new point that's
// closer to the POI taking care not to introduce a
// short arc
if((i +
1) < points.
size()) {
Coord p1 = points.
get(i +
1);
if(p1
instanceof CoordPOI
) {
CoordPOI cp =
(CoordPOI
)p1
;
Node node = cp.
getNode();
if(node.
getTag("access") !=
null) {
// check if this point is further away
// from the POI than we would like
double dist = p.
distance(p1
);
if(dist
>=
(2 * stubSegmentLength
)) {
// insert a new point to make a short
// stub segment
p1 = p1.
makeBetweenPoint(p, stubSegmentLength / dist
);
points.
add(i +
1, p1
);
// as p1 is now no longer a CoordPOI,
// the split below will be deferred
// until the next iteration of the
// loop (which is what we want!)
}
// now split the way here if it is not the
// first point in the way
if(p1
instanceof CoordPOI
&&
i
> 0 &&
!p.
equals(points.
get(i -
1)) &&
!p.
equals(p1
)) {
Way tail = splitWayAt
(way, i
);
// recursively process tail of road
addRoad
(tail, gt
);
}
}
}
}
}
}
// 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) {
clippedWays =
new ArrayList<Way
>();
for (List<Coord
> lco : lineSegs
) {
Way nWay =
new Way
(way.
getId());
nWay.
setName(way.
getName());
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 cw : clippedWays
) {
// make sure the way has nodes at each end
cw.
getPoints().
get(0).
incHighwayCount();
cw.
getPoints().
get(cw.
getPoints().
size() -
1).
incHighwayCount();
addRoadAfterSplittingLoops
(cw, gt
);
}
}
else {
// no bounding box or way was not clipped
// make sure the way has nodes at each end
way.
getPoints().
get(0).
incHighwayCount();
way.
getPoints().
get(way.
getPoints().
size() -
1).
incHighwayCount();
addRoadAfterSplittingLoops
(way, gt
);
}
}
void addRoadAfterSplittingLoops
(Way way, GType gt
) {
// 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
);
for(int p2I = p1I +
1; !wayWasSplit
&& p2I
< numPointsInWay
; p2I++
) {
if(p1 == wayPoints.
get(p2I
)) {
// 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 = p2I -
1;
while(splitI
> p1I
&&
!safeToSplitWay
(wayPoints, splitI, p1I, p2I
)) {
log.
info("Looped way " + getDebugName
(way
) +
" can't safely split at point[" + splitI +
"], trying the preceeding point");
--splitI
;
}
if(splitI == p1I
) {
log.
warn("Splitting looped way " + getDebugName
(way
) +
" 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 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
log.
info("Splitting looped way " + getDebugName
(way
) +
" at " + wayPoints.
get(splitI
).
toOSMURL() +
" - it has " +
(numPointsInWay - splitI -
1 ) +
" following segment(s).");
Way loopTail = splitWayAt
(way, splitI
);
// recursively check (shortened) head for
// more loops
addRoadAfterSplittingLoops
(way, gt
);
// now process the tail of the way
way = loopTail
;
wayWasSplit =
true;
}
}
}
}
if(!wayWasSplit
) {
// no split required so make road from way
addRoadWithoutLoops
(way, gt
);
}
}
}
// safeToSplitWay() returns true if it 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
//
// points - the way's points
// pos - the position we are testing
// floor - lower limit of points to test (inclusive)
// ceiling - upper limit of points to test (inclusive)
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;
}
else if(!candidate.
equals(p
)) {
// 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;
}
else if(!candidate.
equals(p
)) {
// no need to test further
break;
}
}
return true;
}
String getDebugName
(Way way
) {
String name = way.
getName();
if(name ==
null)
name = way.
getTag("ref");
if(name ==
null)
name =
"";
else
name +=
" ";
return name +
"(OSM id " + way.
getId() +
")";
}
@
SuppressWarnings({"AssignmentToForLoopParameter"})
void addRoadWithoutLoops
(Way way, GType gt
) {
List<Integer> nodeIndices =
new ArrayList<Integer>();
List<Coord
> points = way.
getPoints();
Way trailingWay =
null;
String debugWayName = getDebugName
(way
);
// collect the Way's nodes and also split the way if any
// inter-node arc length becomes excessive
double arcLength =
0;
int numPointsInArc =
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
);
// flag that's set true when we back up to a previous node
// while finding a good place to split the line
boolean splitAtPreviousNode =
false;
// 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
);
int numPointsRemaining = points.
size() - i
;
// 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
);
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
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
log.
info("Splitting way " + debugWayName +
" at " + points.
get(i
).
toOSMURL() +
" to limit the size of its bounding box");
}
else if(numPointsInArc
>= MAX_POINTS_IN_ARC
&&
p.
getHighwayCount() < 2) {
// we have to introduce a node here or earlier
// search backwards for a safe place
int nodeI = i
;
int npia = numPointsInArc
;
while(nodeI
> 0 &&
!safeToSplitWay
(points, nodeI, i - numPointsInArc -
1, points.
size() -
1)) {
--nodeI
;
--npia
;
}
// make point into a node
p = points.
get(nodeI
);
p.
incHighwayCount();
log.
info("Making node in " + debugWayName +
" at " + p.
toOSMURL() +
" to limit number of points in arc to " + npia +
", way has " +
(points.
size() - nodeI
) +
" more points");
i = nodeI
; // hack alert! modify loop index
arcLength = p.
distance(points.
get(i +
1));
numPointsInArc =
1;
}
else if(i
> 0 &&
(i + numPointsRemaining +
1) > MAX_POINTS_IN_WAY
&&
numPointsRemaining
<= MAX_POINTS_IN_WAY
&&
p.
getHighwayCount() > 1) {
// if there happens to be no more nodes following
// this one, the way will have to be split
// somewhere otherwise it will be too long so may
// as well split it here
log.
info("Splitting way " + debugWayName +
" at " + points.
get(i
).
toOSMURL() +
" (using an existing node) to limit number of points in this way to " +
(i +
1) +
", way has " + numPointsRemaining +
" more points");
assert trailingWay ==
null :
"trailingWay not null #5";
trailingWay = splitWayAt
(way, i
);
// this will have truncated the current Way's
// points so the loop will now terminate
}
else if(i
>=
(MAX_POINTS_IN_WAY-
1)) {
// we have to split the way here or earlier
// search backwards for a safe place to split the way
int splitI = i
;
while(splitI
> 0 &&
points.
get(splitI
).
getHighwayCount() < 2 &&
!safeToSplitWay
(points, splitI, i - numPointsInArc -
1, points.
size() -
1))
--splitI
;
if(points.
get(i
).
getHighwayCount() > 1) {
// the current point is going to be a node
// anyway so split right here
log.
info("Splitting way " + debugWayName +
" at " + points.
get(i
).
toOSMURL() +
" (would be a node anyway) to limit number of points in this way to " +
(i +
1) +
", way has " +
(points.
size() - i
) +
" more points");
assert trailingWay ==
null :
"trailingWay not null #6a";
trailingWay = splitWayAt
(way, i
);
// this will have truncated the current Way's
// points so the loop will now terminate
}
else if(points.
get(splitI
).
getHighwayCount() > 1) {
// we have found an existing node, use that
log.
info("Splitting way " + debugWayName +
" at " + points.
get(splitI
).
toOSMURL() +
" (using an existing node) to limit number of points in this way to " +
(splitI +
1) +
", way has " +
(points.
size() - splitI
) +
" more points");
assert trailingWay ==
null :
"trailingWay not null #6b";
trailingWay = splitWayAt
(way, splitI
);
// this will have truncated the current Way's
// points so the loop will now terminate
p = points.
get(splitI
);
i = splitI
; // hack alert! modify loop index
// note that we have split the line at a node
// that has already been processed
splitAtPreviousNode =
true;
}
else if(splitI
> 0) {
log.
info("Splitting way " + debugWayName +
" at " + points.
get(splitI
).
toOSMURL() +
" (making a new node) to limit number of points in this way to " +
(splitI +
1) +
", way has " +
(points.
size() - splitI
) +
" more points");
assert trailingWay ==
null :
"trailingWay not null #6c";
trailingWay = splitWayAt
(way, splitI
);
// this will have truncated the current Way's
// points so the loop will now terminate
p = points.
get(splitI
);
i = splitI
; // hack alert! modify loop index
}
else {
log.
error("Way " + debugWayName +
" at " + points.
get(i
).
toOSMURL() +
" has too many points (" + points.
size() +
") but I can't find a safe place to split the way - something's badly wrong here!");
return;
}
}
else {
if(p.
getHighwayCount() > 1) {
// point is a node so zero arc length
arcLength =
0;
numPointsInArc =
0;
}
arcLength += d
;
++numPointsInArc
;
}
}
if(p.
getHighwayCount() > 1) {
// this point is a node connecting highways
Integer nodeId = nodeIdMap.
get(p
);
if(nodeId ==
null) {
// assign a node id
nodeIdMap.
put(p, nextNodeId++
);
}
if(splitAtPreviousNode
) {
// consistency check - this node index should
// already be recorded
assert nodeIndices.
contains(i
) : debugWayName +
" has backed up to point " + i +
" but can't find a node for that point " + p.
toOSMURL();
}
else {
// 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
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, gt, way
);
line.
setPoints(points
);
MapRoad road =
new MapRoad
(way.
getId(), line
);
if (way.
isBoolTag("mkgmap:skipSizeFilter"))
road.
setSkipSizeFilter(true);
boolean doFlareCheck =
true;
if("roundabout".
equals(way.
getTag("junction"))) {
road.
setRoundabout(true);
doFlareCheck =
false;
}
if(way.
isBoolTag("mkgmap:synthesised")) {
road.
setSynthesised(true);
doFlareCheck =
false;
}
if(way.
isNotBoolTag("mkgmap:flare-check")) {
doFlareCheck =
false;
}
else if(way.
isBoolTag("mkgmap:flare-check")) {
doFlareCheck =
true;
}
road.
doFlareCheck(doFlareCheck
);
road.
setLinkRoad(gt.
getType() == 0x08 || gt.
getType() == 0x09
);
// set road parameters
// road class (can be overridden by mkgmap:road-class tag)
int roadClass = gt.
getRoadClass();
String val = way.
getTag("mkgmap:road-class");
if(val
!=
null) {
if(val.
startsWith("-")) {
roadClass -=
Integer.
decode(val.
substring(1));
}
else if(val.
startsWith("+")) {
roadClass +=
Integer.
decode(val.
substring(1));
}
else {
roadClass =
Integer.
decode(val
);
}
val = way.
getTag("mkgmap:road-class-max");
int roadClassMax =
4;
if(val
!=
null)
roadClassMax =
Integer.
decode(val
);
val = way.
getTag("mkgmap:road-class-min");
int roadClassMin =
0;
if(val
!=
null)
roadClassMin =
Integer.
decode(val
);
if(roadClass
> roadClassMax
)
roadClass = roadClassMax
;
else if(roadClass
< roadClassMin
)
roadClass = roadClassMin
;
log.
info("POI changing road class of " + way.
getName() +
" (" + way.
getId() +
") to " + roadClass +
" at " + points.
get(0));
}
road.
setRoadClass(roadClass
);
// road speed (can be overridden by maxspeed (OSM) tag or
// mkgmap:road-speed tag)
int roadSpeed = gt.
getRoadSpeed();
if(!ignoreMaxspeeds
) {
// maxspeed attribute overrides default for road type
String maxSpeed = way.
getTag("maxspeed");
if(maxSpeed
!=
null) {
int rs = getSpeedIdx
(maxSpeed
);
if(rs
>=
0)
roadSpeed = rs
;
log.
debug(debugWayName +
" maxspeed=" + maxSpeed +
", speedIndex=" + roadSpeed
);
}
}
val = way.
getTag("mkgmap:road-speed");
if(val
!=
null) {
if(val.
startsWith("-")) {
roadSpeed -=
Integer.
decode(val.
substring(1));
}
else if(val.
startsWith("+")) {
roadSpeed +=
Integer.
decode(val.
substring(1));
}
else {
roadSpeed =
Integer.
decode(val
);
}
val = way.
getTag("mkgmap:road-speed-max");
int roadSpeedMax =
7;
if(val
!=
null)
roadSpeedMax =
Integer.
decode(val
);
val = way.
getTag("mkgmap:road-speed-min");
int roadSpeedMin =
0;
if(val
!=
null)
roadSpeedMin =
Integer.
decode(val
);
if(roadSpeed
> roadSpeedMax
)
roadSpeed = roadSpeedMax
;
else if(roadSpeed
< roadSpeedMin
)
roadSpeed = roadSpeedMin
;
log.
info("POI changing road speed of " + way.
getName() +
" (" + way.
getId() +
") to " + roadSpeed +
" at " + points.
get(0));
}
road.
setSpeed(roadSpeed
);
if (way.
isBoolTag("oneway")) {
road.
setDirection(true);
road.
setOneway();
if (checkFixmeCoords
(way
))
way.
addTag("mkgmap:dead-end-check",
"false");
road.
doDeadEndCheck(!way.
isNotBoolTag("mkgmap:dead-end-check"));
}
String highwayType = way.
getTag("highway");
if(highwayType ==
null) {
// it's a routable way but not a highway (e.g. a ferry)
// use the value of the route tag as the highwayType for
// the purpose of testing for access restrictions
highwayType = way.
getTag("route");
}
boolean[] noAccess =
new boolean[RoadNetwork.
NO_MAX];
for (AccessMapping anAccessMap : accessMap
) {
int index = anAccessMap.
index;
String type = anAccessMap.
type;
String accessTagValue = way.
getTag(type
);
if (accessTagValue ==
null)
continue;
if (accessExplicitlyDenied
(accessTagValue
)) {
if (index == RoadNetwork.
NO_MAX) {
// everything is denied access
for (int j =
1; j
< accessMap.
length; ++j
)
noAccess
[accessMap
[j
].
index] =
true;
} else {
// just the specific vehicle class is denied
// access
noAccess
[index
] =
true;
}
log.
debug(type +
" is not allowed in " + highwayType +
" " + debugWayName
);
} else if (accessExplicitlyAllowed
(accessTagValue
)) {
if (index == RoadNetwork.
NO_MAX) {
// everything is allowed access
for (int j =
1; j
< accessMap.
length; ++j
)
noAccess
[accessMap
[j
].
index] =
false;
} else {
// just the specific vehicle class is allowed
// access
noAccess
[index
] =
false;
}
log.
debug(type +
" is allowed in " + highwayType +
" " + debugWayName
);
}
else if (accessTagValue.
equalsIgnoreCase("destination")) {
if (type.
equals("motorcar") ||
type.
equals("motorcycle")) {
road.
setNoThroughRouting();
} else if (type.
equals("access")) {
log.
info("access=destination only affects routing for cars in " + highwayType +
" " + debugWayName
);
road.
setNoThroughRouting();
} else {
log.
info(type +
"=destination ignored in " + highwayType +
" " + debugWayName
);
}
} else if (accessTagValue.
equalsIgnoreCase("unknown")) {
// implicitly allow access
} else {
log.
info("Ignoring unsupported access tag value " + type +
"=" + accessTagValue +
" in " + highwayType +
" " + debugWayName
);
}
}
if(way.
isBoolTag("mkgmap:carpool")) {
// to make a way into a "carpool lane" all access disable
// bits must be set except for CARPOOL and EMERGENCY (BUS
// can also be clear)
road.
setNoThroughRouting();
for (int j =
1; j
< accessMap.
length; ++j
)
noAccess
[accessMap
[j
].
index] =
true;
noAccess
[RoadNetwork.
NO_CARPOOL] =
false;
noAccess
[RoadNetwork.
NO_EMERGENCY] =
false;
noAccess
[RoadNetwork.
NO_BUS] =
false;
}
road.
setAccess(noAccess
);
if(way.
isBoolTag("toll"))
road.
setToll();
// by default, ways are paved
if(way.
isBoolTag("mkgmap:unpaved"))
road.
paved(false);
// by default, way's are not ferry routes
if(way.
isBoolTag("mkgmap:ferry"))
road.
ferry(true);
int numNodes = nodeIndices.
size();
road.
setNumNodes(numNodes
);
if(numNodes
> 0) {
// replace Coords that are nodes with CoordNodes
boolean hasInternalNodes =
false;
CoordNode lastCoordNode =
null;
List<RestrictionRelation
> lastRestrictions =
null;
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
);
Integer nodeId = nodeIdMap.
get(coord
);
assert nodeId
!=
null :
"Way " + debugWayName +
" node " + i +
" (point index " + n +
") at " + coord.
toOSMURL() +
" yields a null node id";
boolean boundary = coord.
getOnBoundary();
if(boundary
) {
log.
info("Way " + debugWayName +
"'s point #" + n +
" at " + points.
get(n
).
toDegreeString() +
" is a boundary node");
}
CoordNode thisCoordNode =
new CoordNode
(coord.
getLatitude(), coord.
getLongitude(), nodeId, boundary
);
points.
set(n, thisCoordNode
);
// see if this node plays a role in any turn
// restrictions
if(lastRestrictions
!=
null) {
// the previous node was the location of one or
// more restrictions
for(RestrictionRelation rr : lastRestrictions
) {
if(rr.
getToWay().
getId() == way.
getId()) {
rr.
setToNode(thisCoordNode
);
}
else if(rr.
getFromWay().
getId() == way.
getId()) {
rr.
setFromNode(thisCoordNode
);
}
else {
rr.
addOtherNode(thisCoordNode
);
}
}
}
List<RestrictionRelation
> theseRestrictions = restrictions.
get(coord
);
if(theseRestrictions
!=
null) {
// this node is the location of one or more
// restrictions
for(RestrictionRelation rr : theseRestrictions
) {
rr.
setViaNode(thisCoordNode
);
if(rr.
getToWay().
getId() == way.
getId()) {
if(lastCoordNode
!=
null)
rr.
setToNode(lastCoordNode
);
}
else if(rr.
getFromWay().
getId() == way.
getId()) {
if(lastCoordNode
!=
null)
rr.
setFromNode(lastCoordNode
);
}
else if(lastCoordNode
!=
null) {
rr.
addOtherNode(lastCoordNode
);
}
}
}
lastRestrictions = theseRestrictions
;
lastCoordNode = thisCoordNode
;
}
road.
setStartsWithNode(nodeIndices.
get(0) ==
0);
road.
setInternalNodes(hasInternalNodes
);
}
// add the road to the housenumber generator
// it will add the road later on to the lineAdder
housenumberGenerator.
addRoad(road
);
// lineAdder.add(road);
if(trailingWay
!=
null)
addRoadWithoutLoops
(trailingWay, gt
);
}
/**
* 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 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)
Way splitWayAt
(Way way,
int index
) {
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.
setName(way.
getName());
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
;
}
// function to add points between adjacent nodes in a roundabout
// to make gps use correct exit number in routing instructions
void frigRoundabout
(Way way,
double frigFactor
) {
List<Coord
> wayPoints = way.
getPoints();
int origNumPoints = wayPoints.
size();
if(origNumPoints
< 3) {
// forget it!
return;
}
int[] highWayCounts =
new int[origNumPoints
];
highWayCounts
[0] = wayPoints.
get(0).
getHighwayCount();
int middleLat =
0;
int middleLon =
0;
for(int i =
1; i
< origNumPoints
; ++i
) {
Coord p = wayPoints.
get(i
);
middleLat += p.
getLatitude();
middleLon += p.
getLongitude();
highWayCounts
[i
] = p.
getHighwayCount();
}
middleLat /= origNumPoints -
1;
middleLon /= origNumPoints -
1;
Coord middleCoord =
new Coord
(middleLat, middleLon
);
// account for fact that roundabout joins itself
--highWayCounts
[0];
--highWayCounts
[origNumPoints -
1];
for(int i = origNumPoints -
2; i
>=
0; --i
) {
Coord p1 = wayPoints.
get(i
);
Coord p2 = wayPoints.
get(i +
1);
if(highWayCounts
[i
] > 1 && highWayCounts
[i +
1] > 1) {
// both points will be nodes so insert a new point
// between them that (approximately) falls on the
// roundabout's perimeter
int newLat =
(p1.
getLatitude() + p2.
getLatitude()) /
2;
int newLon =
(p1.
getLongitude() + p2.
getLongitude()) /
2;
// new point has to be "outside" of existing line
// joining p1 and p2 - how far outside is determined
// by the ratio of the distance between p1 and p2
// compared to the distance of p1 from the "middle" of
// the roundabout (aka, the approx radius of the
// roundabout) - the higher the value of frigFactor,
// the further out the point will be
double scale =
1 + frigFactor
* p1.
distance(p2
) / p1.
distance(middleCoord
);
newLat =
(int)((newLat - middleLat
) * scale
) + middleLat
;
newLon =
(int)((newLon - middleLon
) * scale
) + middleLon
;
Coord newPoint =
new Coord
(newLat, newLon
);
double d1 = p1.
distance(newPoint
);
double d2 = p2.
distance(newPoint
);
double maxDistance =
100;
if(d1
>= MIN_DISTANCE_BETWEEN_NODES
&& d1
<= maxDistance
&&
d2
>= MIN_DISTANCE_BETWEEN_NODES
&& d2
<= maxDistance
) {
newPoint.
incHighwayCount();
wayPoints.
add(i +
1, newPoint
);
}
}
}
}
private int getSpeedIdx
(String tag
) {
double factor =
1.0;
String speedTag = tag.
toLowerCase().
trim();
if (ENDS_IN_MPH_PATTERN.
matcher(speedTag
).
matches()) {
// Check if it is a limit in mph
speedTag = REMOVE_MPH_PATTERN.
matcher(speedTag
).
replaceFirst("");
factor =
1.61;
} else
speedTag = REMOVE_KPH_PATTERN.
matcher(speedTag
).
replaceFirst(""); // get rid of kmh just in case
double kmh
;
try {
kmh =
Integer.
parseInt(speedTag
) * factor
;
} catch (Exception e
) {
return -
1;
}
if(kmh
> 110)
return 7;
if(kmh
> 90)
return 6;
if(kmh
> 80)
return 5;
if(kmh
> 60)
return 4;
if(kmh
> 40)
return 3;
if(kmh
> 20)
return 2;
if(kmh
> 10)
return 1;
else
return 0;
}
protected boolean accessExplicitlyAllowed
(String val
) {
if (val ==
null)
return false;
return (val.
equalsIgnoreCase("yes") ||
val.
equalsIgnoreCase("designated") ||
val.
equalsIgnoreCase("permissive") ||
val.
equalsIgnoreCase("official"));
}
protected boolean accessExplicitlyDenied
(String val
) {
if (val ==
null)
return false;
return (val.
equalsIgnoreCase("no") ||
val.
equalsIgnoreCase("private"));
}
/**
* 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;
for (Way way :roads
){
if (way.
getId() == lastId
)
continue;
lastId = way.
getId();
List<Coord
> points = way.
getPoints();
for (Coord p:points
){
p.
incHighwayCount();
}
}
}
/**
* 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<Coord,
HashSet<Way
>>(roads.
size()*2);
// collect nodes that might connect roads
long lastId =
0;
for (Way way :roads
){
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<Way
>(4);
connectors.
put(p, ways
);
}
ways.
add(way
);
}
}
}
// find roads that are not connected
for (int i =
0; i
< roads.
size(); i++
){
Way way = roads.
get(i
);
String check_type = way.
getTag("mkgmap:set_unconnected_type");
if (check_type
!=
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(check_type
))
log.
info("road not connected to other roads, is ignored: " + way.
toBrowseURL());
else {
int type = -
1;
try{
type =
Integer.
decode(check_type
);
if (GType.
isRoutableLineType(type
)){
type = -
1;
log.
error("type value in mkgmap:set_unconnected_type should not be a routable type: " + check_type
);
}
} catch (NumberFormatException e
){
log.
warn("invalid type value in mkgmap:set_unconnected_type: " + check_type
);
}
if (type
!= -
1 ){
log.
info("road not connected to other roads, added as line with type " + check_type +
": " + way.
toBrowseURL());
GType gt =
new GType
(roadTypes.
get(i
), check_type
);
addLine
(way, gt
);
} else {
log.
warn("road not connected to other roads, but replacement type is invalid. Dropped: " + way.
toBrowseURL());
}
}
roads.
set(i,
null);
roadTypes.
set(i,
null);
deletedRoads.
add(way.
getId()); // XXX Maybe not if road is changed to a line?
}
}
}
}
}
private void removeShortArcsByMergingNodes
(double minArcLength
) {
log.
info("Removing short arcs (min arc length = " + minArcLength +
"m)");
log.
info("Removing short arcs - marking points as node-alike");
for (Way way : roads
) {
if (way ==
null)
continue;
List<Coord
> points = way.
getPoints();
int numPoints = points.
size();
if (numPoints
>=
2) {
// all end points should be treated as nodes
points.
get(0).
setTreatAsNode(true);
points.
get(numPoints -
1).
setTreatAsNode(true);
// non-end points have 2 arcs but ignore points that
// are only in a single way
for (int i = numPoints -
2; i
>=
1; --i
) {
Coord p = points.
get(i
);
// if this point is a CoordPOI it may become a
// node later even if it isn't actually a connection
// between roads at this time - so for the purposes
// of short arc removal, consider it to be a node
// if it is on a boundary it will become a node later
if (p.
getHighwayCount() > 1 || p
instanceof CoordPOI || p.
getOnBoundary())
p.
setTreatAsNode(true);
}
}
}
// replacements maps those nodes that have been replaced to
// the node that replaces them
Map<Coord, Coord
> replacements =
new IdentityHashMap<Coord, Coord
>();
Map<Way, Way
> complainedAbout =
new HashMap<Way, Way
>();
boolean anotherPassRequired =
true;
int pass =
0;
int numWaysDeleted =
0;
int numNodesMerged =
0;
while (anotherPassRequired
&& pass
< 10) {
anotherPassRequired =
false;
log.
info("Removing short arcs - PASS " + ++pass
);
for (int w =
0; w
< roads.
size(); w++
){
Way way = roads.
get(w
);
if (way ==
null)
continue;
List<Coord
> points = way.
getPoints();
if (points.
size() < 2) {
if (log.
isInfoEnabled())
log.
info(" Way " + way.
getTag("name") +
" (" + way.
toBrowseURL() +
") has less than 2 points - deleting it");
roads.
set(w,
null);
deletedRoads.
add(way.
getId());
++numWaysDeleted
;
continue;
}
// scan through the way's points looking for nodes and
// check to see that the nodes are not too close to
// each other
int previousNodeIndex =
0; // first point will be a node
Coord previousPoint = points.
get(0);
double arcLength =
0;
for (int i =
0; i
< points.
size(); ++i
) {
Coord p = points.
get(i
);
// check if this point is to be replaced because
// it was previously merged into another point
if (p.
isReplaced()){
Coord replacement =
null;
Coord r = p
;
while ((r = replacements.
get(r
)) !=
null) {
replacement = r
;
}
if (replacement
!=
null) {
assert !p.
getOnBoundary() :
"Boundary node replaced";
p = replacement
;
// replace point in way
points.
set(i, p
);
if (i ==
0)
previousPoint = p
;
modifiedRoads.
put(way.
getId(), way
);
anotherPassRequired =
true;
}
}
if (i ==
0) {
// nothing more to do with this point
continue;
}
// this is not the first point in the way
if (p == previousPoint
) {
if (log.
isInfoEnabled())
log.
info(" Way " + way.
getTag("name") +
" (" + way.
toBrowseURL() +
") has consecutive identical points at " + p.
toOSMURL() +
" - deleting the second point");
points.
remove(i
);
// hack alert! rewind the loop index
--i
;
modifiedRoads.
put(way.
getId(), way
);
anotherPassRequired =
true;
continue;
}
if (minArcLength
> 0){
// we have to calculate the length of the arc
arcLength += p.
distance(previousPoint
);
}
else {
// if the points are not equal, the arc length is > 0
if (!p.
equals(previousPoint
)){
arcLength =
1; // just a value > 0
}
}
previousPoint = p
;
// do we treat this point as a node ?
if (!p.
isTreatAsNode()) {
// it's not a node so go on to next point
continue;
}
Coord previousNode = points.
get(previousNodeIndex
);
if (p == previousNode
) {
// this node is the same point object as the
// previous node - leave it for now and it
// will be handled later by the road loop
// splitter
previousNodeIndex = i
;
arcLength =
0;
continue;
}
boolean mergeNodes =
false;
if (p.
equals(previousNode
)) {
// nodes have identical coordinates and are
// candidates for being merged
// however, to avoid trashing unclosed loops
// (e.g. contours) we only want to merge the
// nodes when the length of the arc between
// the nodes is small
if(arcLength ==
0 || arcLength
< minArcLength
)
mergeNodes =
true;
else if(complainedAbout.
get(way
) ==
null) {
if (log.
isInfoEnabled())
log.
info(" Way " + way.
getTag("name") +
" (" + way.
toBrowseURL() +
") has unmerged co-located nodes at " + p.
toOSMURL() +
" - they are joined by a " +
(int)(arcLength
* 10) /
10.0 +
"m arc");
complainedAbout.
put(way, way
);
}
}
else if(minArcLength
> 0 && minArcLength
> arcLength
) {
// nodes have different coordinates but the
// arc length is less than minArcLength so
// they will be merged
mergeNodes =
true;
}
if (!mergeNodes
) {
// keep this node and go look at the next point
previousNodeIndex = i
;
arcLength =
0;
continue;
}
if (previousNode.
getOnBoundary() && p.
getOnBoundary()) {
if (p.
equals(previousNode
)) {
// the previous node has identical
// coordinates to the current node so it
// can be replaced but to avoid the
// assertion above we need to forget that
// it is on the boundary
previousNode.
setOnBoundary(false);
} else {
// both the previous node and this node
// are on the boundary and they don't have
// identical coordinates
if(complainedAbout.
get(way
) ==
null) {
if (log.
isLoggable(Level.
WARNING))
log.
warn(" Way " + way.
getTag("name") +
" (" + way.
toBrowseURL() +
") has short arc (" +
String.
format("%.2f", arcLength
) +
"m) at " + p.
toOSMURL() +
" - but it can't be removed because both ends of the arc are boundary nodes!");
complainedAbout.
put(way, way
);
}
break; // give up with this way
}
}
// reset arc length
arcLength =
0;
// do the merge
++numNodesMerged
;
if (p.
getOnBoundary()) {
// current point is a boundary node so we need
// to merge the previous node into this node
replacements.
put(previousNode, p
);
previousNode.
setReplaced(true);
p.
setTreatAsNode(true);
// remove the preceding point(s) back to and
// including the previous node
for(int j = i -
1; j
>= previousNodeIndex
; --j
) {
points.
remove(j
);
}
} else {
// current point is not on a boundary so merge
// this node into the previous one
replacements.
put(p, previousNode
);
p.
setReplaced(true);
previousNode.
setTreatAsNode(true);
// reset previous point to be the previous
// node
previousPoint = previousNode
;
// remove the point(s) back to the previous
// node
for (int j = i
; j
> previousNodeIndex
; --j
) {
points.
remove(j
);
}
}
// hack alert! rewind the loop index
i = previousNodeIndex
;
modifiedRoads.
put(way.
getId(), way
);
anotherPassRequired =
true;
}
}
}
if (anotherPassRequired
)
log.
error("Removing short arcs - didn't finish in " + pass +
" passes, giving up!");
else
log.
info("Removing short arcs - finished in", pass,
"passes (", numNodesMerged,
"nodes merged,", numWaysDeleted,
"ways deleted)");
}
}