Imported Upstream version 0.0.0+svn3649
Bas Couwenberg
8 years ago
0 | 0 | <?xml version="1.0"?> |
1 | 1 | <!-- |
2 | File: build.xml | |
2 | File: build.xml | |
3 | 3 | |
4 | 4 | Copyright (C) 2006, 2012 mkgmap contributors |
5 | 5 |
433 | 433 | |
434 | 434 | |
435 | 435 | ;--adjust-turn-headings[=BITMASK] |
436 | : Where possible, ensure that turns off to side roads change | |
436 | : now ignored, former explanation: | |
437 | Where possible, ensure that turns off to side roads change | |
437 | 438 | heading sufficiently so that the GPS believes that a turn is |
438 | 439 | required rather than a fork. This also avoids spurious |
439 | 440 | instructions to "keep right/left" when the road doesn't |
444 | 445 | :* 1 = increase angle between side road and outgoing main road |
445 | 446 | :* 2 = increase angle between side road and incoming main road |
446 | 447 | |
448 | ;--cycle-map | |
449 | : Tells mkgmap that the map is for cyclists. This assumes that | |
450 | different vehicles are different kinds of bicycles, e.g. a way | |
451 | with mkgmap:car=yes and mkgmap:bicycle=no may be a road that is | |
452 | good for racing bikes, but not for other cyclists. | |
453 | This allows to optimise sharp angles at junctions of those roads. | |
454 | Don't use with the default style as that is a general style! | |
447 | 455 | |
448 | 456 | ;--report-similar-arcs |
449 | 457 | : Issue a warning when more than one arc connects two nodes and |
146 | 146 | <variant>BV</variant> |
147 | 147 | <variant>BVT</variant> |
148 | 148 | </country> |
149 | <country name="Brazil" abr="BRA" streetBeforeHousenumber="true"> | |
149 | <country name="Brasil" abr="BRA" streetBeforeHousenumber="true"> | |
150 | 150 | <variant>BR</variant> |
151 | 151 | <variant>BRA</variant> |
152 | <variant>Brazil</variant> | |
152 | 153 | </country> |
153 | 154 | <country name="British Indian Ocean Territory" abr="IOT"> |
154 | 155 | <variant>IO</variant> |
436 | 436 | specified, only zero-length arcs will be removed. |
437 | 437 | |
438 | 438 | --adjust-turn-headings[=BITMASK] |
439 | Now ignored, former usage: | |
439 | 440 | Where possible, ensure that turns off to side roads change |
440 | 441 | heading sufficiently so that the GPS believes that a turn is |
441 | 442 | required rather than a fork. This also avoids spurious |
448 | 449 | 1 = increase angle between side road and outgoing main road |
449 | 450 | 2 = increase angle between side road and incoming main road |
450 | 451 | |
452 | --cycle-map | |
453 | Tells mkgmap that the map is for cyclists. This assumes that | |
454 | different vehicles are different kinds of bicycles, e.g. a way | |
455 | with mkgmap:car=yes and mkgmap:bicycle=no may be a road that is | |
456 | good for racing bikes, but not for other cyclists. | |
457 | This allows to optimise sharp angles at junctions of those roads. | |
458 | Don't use with the default style as that is a general style! | |
459 | ||
451 | 460 | --report-similar-arcs |
452 | 461 | Issue a warning when more than one arc connects two nodes and |
453 | 462 | the ways that the arcs are derived from contain identical |
0 | svn.version: 3643 | |
1 | build.timestamp: 2015-09-19T08:13:00+0100 | |
0 | svn.version: 3649 | |
1 | build.timestamp: 2015-10-27T06:51:19+0000 |
71 | 71 | # Check for carpool lane (they are not really supported yet so these lines are commented) |
72 | 72 | # hov=* { add carpool='${hov}' } |
73 | 73 | # (carpool=yes | carpool=designated | carpool=permissive | carpool=official) { set mkgmap:carpool=yes } |
74 | ||
75 | # Don't route through highway=construction, they are considered unusable | |
76 | highway=construction {setaccess no} | |
77 |
17 | 17 | # Mark highways with the toll flag |
18 | 18 | highway=* & (toll=yes|toll=true) { set mkgmap:toll=yes } |
19 | 19 | |
20 | # Hide proposed ways | |
21 | highway=proposed {delete highway;delete junction} | |
20 | # Hide proposed ways | |
21 | (highway=proposed | highway=proposal | highway=planned | highway ~ '.*proposed.*') {delete highway;delete junction} | |
22 | 22 | # Hide removed ways |
23 | highway=razed {deletealltags} | |
23 | (highway=razed | highway=dismantled) {deletealltags} | |
24 | # Hide other non-existent ways | |
25 | (highway=unbuilt | highway=neverbuilt | highway=rejected | highway ~ 'x-.*') {delete highway;delete junction} | |
26 | ||
24 | 27 | # Hide unaccessible tunnels |
25 | 28 | highway=* & tunnel=yes & (access=private|access=no) |
26 | 29 | & foot!=* & bicycle!=* {delete highway;delete junction} |
105 | 108 | |
106 | 109 | # Ways that may or may not be useable |
107 | 110 | |
108 | # Treat ways under construction almost as highway=path | |
111 | # Treat ways under construction almost as highway=path, see also extra rule in inc/access | |
109 | 112 | highway=construction { add mkgmap:dead-end-check = false; } |
110 | 113 | [0x16 road_class=0 road_speed=0 resolution 23] |
111 | 114 |
48 | 48 | private final static short PART_OF_SHAPE2 = 0x0100; // use only in ShapeMerger |
49 | 49 | private final static short END_OF_WAY = 0x0200; // use only in WrongAngleFixer |
50 | 50 | private final static short HOUSENUMBER_NODE = 0x0400; // start/end of house number interval |
51 | private final static short ADDED_HOUSENUMBER_NODE = 0x0800; // node was added for house numbers | |
51 | 52 | |
52 | 53 | public final static int HIGH_PREC_BITS = 30; |
53 | 54 | public final static int DELTA_SHIFT = 6; |
87 | 88 | int lon30 = toBit30(longitude); |
88 | 89 | this.latDelta = (byte) ((this.latitude << 6) - lat30); |
89 | 90 | this.lonDelta = (byte) ((this.longitude << 6) - lon30); |
90 | ||
91 | ||
91 | 92 | // verify math |
92 | 93 | assert (this.latitude << 6) - latDelta == lat30; |
93 | 94 | assert (this.longitude << 6) - lonDelta == lon30; |
339 | 340 | } |
340 | 341 | |
341 | 342 | /** |
342 | * Set or unset flag for {@link WrongAngleFixer} | |
343 | 343 | * @param b true or false |
344 | 344 | */ |
345 | 345 | public void setNumberNode(boolean b) { |
347 | 347 | this.flags |= HOUSENUMBER_NODE; |
348 | 348 | else |
349 | 349 | this.flags &= ~HOUSENUMBER_NODE; |
350 | } | |
351 | ||
352 | /** | |
353 | * @return if this is the beginning/end of a house number interval | |
354 | */ | |
355 | public boolean isAddedNumberNode(){ | |
356 | return (flags & ADDED_HOUSENUMBER_NODE) != 0; | |
357 | } | |
358 | ||
359 | /** | |
360 | * @param b true or false | |
361 | */ | |
362 | public void setAddedNumberNode(boolean b) { | |
363 | if (b) | |
364 | this.flags |= ADDED_HOUSENUMBER_NODE; | |
365 | else | |
366 | this.flags &= ~ADDED_HOUSENUMBER_NODE; | |
350 | 367 | } |
351 | 368 | |
352 | 369 | public int hashCode() { |
0 | /* | |
1 | * Copyright (C) 2015 | |
2 | * | |
3 | * This program is free software; you can redistribute it and/or modify | |
4 | * it under the terms of the GNU General Public License version 3 or | |
5 | * version 2 as published by the Free Software Foundation. | |
6 | * | |
7 | * This program is distributed in the hope that it will be useful, but | |
8 | * WITHOUT ANY WARRANTY; without even the implied warranty of | |
9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
10 | * General Public License for more details. | |
11 | */ | |
12 | package uk.me.parabola.imgfmt.app.net; | |
13 | ||
14 | import java.util.ArrayList; | |
15 | import java.util.Collections; | |
16 | import java.util.Comparator; | |
17 | import java.util.HashSet; | |
18 | import java.util.Iterator; | |
19 | import java.util.List; | |
20 | import java.util.Map; | |
21 | ||
22 | import uk.me.parabola.log.Logger; | |
23 | import uk.me.parabola.util.EnhancedProperties; | |
24 | ||
25 | /** | |
26 | * Find sharp angles at junctions. The Garmin routing algorithm doesn't | |
27 | * like to route on roads building a sharp angle. It adds a time penalty | |
28 | * from 30 to 150 seconds and often prefers small detours instead. | |
29 | * The penalty depends on the road speed and the vehicle, for pedestrian | |
30 | * mode it is zero, for bicycles it is rather small, for cars it is high. | |
31 | * The sharp angles typically don't exist in the real world, they are | |
32 | * caused by the simplifications done by mappers. | |
33 | * | |
34 | * Maps created for cyclists typically "abuse" the car routing for racing | |
35 | * bikes, but in this scenario the time penalties are much too high, | |
36 | * and detours are likely. | |
37 | * | |
38 | * This method tries to modify the initial heading values of the arcs | |
39 | * which are used to calculate the angles. Where possible, the values are | |
40 | * changed so that angles appear larger. | |
41 | * | |
42 | * @author Gerd Petermann | |
43 | * | |
44 | */ | |
45 | public class AngleChecker { | |
46 | private static final Logger log = Logger.getLogger(AngleChecker.class); | |
47 | ||
48 | private boolean ignoreSharpAngles; | |
49 | private boolean cycleMap; | |
50 | // private final Coord test = new Coord(48.074815,16.272771); | |
51 | ||
52 | private final int MIN_ANGLE = 0x10; | |
53 | private final int MIN_LOW_SPEED_ANGLE = 0x20; | |
54 | ||
55 | private int mask; | |
56 | ||
57 | // helper class to collect multiple arcs with (nearly) the same initial headings | |
58 | private class ArcGroup { | |
59 | float initialHeading; | |
60 | byte imgHeading; | |
61 | int isOneWayTrueCount; | |
62 | int isForwardTrueCount; | |
63 | int maxRoadSpeed; | |
64 | byte orAccessMask; | |
65 | HashSet<RoadDef> roadDefs = new HashSet<>(); | |
66 | ||
67 | List<RouteArc> arcs = new ArrayList<>(); | |
68 | public void addArc(RouteArc arc) { | |
69 | arcs.add(arc); | |
70 | if (arc.getRoadDef().isOneway()) | |
71 | isOneWayTrueCount++; | |
72 | if (arc.isForward()) | |
73 | isForwardTrueCount++; | |
74 | if (arc.getRoadDef().getRoadSpeed() > maxRoadSpeed) | |
75 | maxRoadSpeed = arc.getRoadDef().getRoadSpeed(); | |
76 | orAccessMask |= arc.getRoadDef().getAccess(); | |
77 | roadDefs.add(arc.getRoadDef()); | |
78 | } | |
79 | public float getInitialHeading() { | |
80 | return initialHeading; | |
81 | } | |
82 | public boolean isOneway() { | |
83 | return isOneWayTrueCount == arcs.size(); | |
84 | } | |
85 | public boolean isForward() { | |
86 | return isForwardTrueCount == arcs.size(); | |
87 | } | |
88 | /** | |
89 | * @return | |
90 | */ | |
91 | public void setInitialHeading(float modIH) { | |
92 | while (modIH > 180) | |
93 | modIH -= 360; | |
94 | while (modIH < -180) | |
95 | modIH += 360; | |
96 | initialHeading = modIH; | |
97 | imgHeading = (byte) (RouteArc.directionFromDegrees(initialHeading) & mask); | |
98 | ||
99 | for (RouteArc arc : arcs){ | |
100 | arc.setInitialHeading(modIH); | |
101 | } | |
102 | } | |
103 | ||
104 | public String toString(){ | |
105 | return arcs.get(0).toString(); | |
106 | } | |
107 | } | |
108 | ||
109 | public void config(EnhancedProperties props) { | |
110 | // undocumented option - usually used for debugging only | |
111 | ignoreSharpAngles = props.getProperty("ignore-sharp-angles", false); | |
112 | cycleMap = props.getProperty("cycle-map", false); | |
113 | // float a = 0; | |
114 | // for (int i = 0; i <= 1440; i++){ | |
115 | // int ar = (int) Math.round(a * 256.0 / 360); | |
116 | // int am = ar & 0xf0; | |
117 | // log.error(a,ar,"0x" + Integer.toHexString(am)); | |
118 | // a +=0.25; | |
119 | // if (a >= 180) | |
120 | // a -= 360; | |
121 | // } | |
122 | return; | |
123 | } | |
124 | ||
125 | public void check(Map<Integer, RouteNode> nodes) { | |
126 | if (!ignoreSharpAngles){ | |
127 | byte sharpAnglesCheckMask = cycleMap ? (byte) (0xff & ~AccessTagsAndBits.FOOT) : AccessTagsAndBits.BIKE; | |
128 | ||
129 | for (RouteNode node : nodes.values()){ | |
130 | mask = 0xf0; // we assume compacted format | |
131 | fixSharpAngles(node, sharpAnglesCheckMask); | |
132 | } | |
133 | } | |
134 | } | |
135 | ||
136 | public void fixSharpAngles(RouteNode node, byte sharpAnglesCheckMask) { | |
137 | ||
138 | // get direct arcs leaving the node | |
139 | List<ArcGroup> arcGroups = buildArcGroups(node); | |
140 | ||
141 | int n = arcGroups.size(); | |
142 | if (n <= 1) | |
143 | return; | |
144 | // sort the arcs by initial heading | |
145 | Collections.sort(arcGroups, new Comparator<ArcGroup>() { | |
146 | public int compare(ArcGroup ag1, ArcGroup ag2) { | |
147 | if (ag1.initialHeading < ag2.initialHeading) | |
148 | return -1; | |
149 | if (ag1.initialHeading > ag2.initialHeading) | |
150 | return 1; | |
151 | return 0; | |
152 | } | |
153 | }); | |
154 | ||
155 | class AngleAttr { | |
156 | int angle; | |
157 | int maskedAngle; | |
158 | int maskedMinAngle = MIN_ANGLE; | |
159 | boolean noAccess; | |
160 | ||
161 | int maskedDeltaToMin(){ | |
162 | return maskedAngle - maskedMinAngle; | |
163 | } | |
164 | void setMaskedMinAngle(int maskedMinAngle){ | |
165 | this.maskedMinAngle = maskedMinAngle; | |
166 | } | |
167 | ||
168 | public String toString(){ | |
169 | return angle + "° " + maskedAngle + " " + maskedMinAngle + " " + noAccess; | |
170 | } | |
171 | } | |
172 | ||
173 | // step one: calculate the existing angles | |
174 | AngleAttr[] angles = new AngleAttr[n]; | |
175 | for (int i = 0; i < n; i++){ | |
176 | ArcGroup ag1 = arcGroups.get(i); | |
177 | ArcGroup ag2 = arcGroups.get(i+1 < n ? i+1 : 0); | |
178 | AngleAttr angleAttr = new AngleAttr(); | |
179 | angles[i] = angleAttr; | |
180 | angleAttr.angle = Math.round(ag2.getInitialHeading() - ag1.getInitialHeading()); | |
181 | angleAttr.maskedAngle = ag2.imgHeading - ag1.imgHeading; | |
182 | if (i + 1 >= n){ | |
183 | angleAttr.angle += 360; | |
184 | } | |
185 | if (angleAttr.maskedAngle < 0) | |
186 | angleAttr.maskedAngle += 256; | |
187 | ||
188 | if (ag1.isOneway() && ag1.isForward()){ | |
189 | // the "incoming" arc is a wrong direction oneway | |
190 | angleAttr.noAccess = true; | |
191 | } else if (ag2.isOneway() && ag2.isForward() == false){ | |
192 | // the "outgoing" arc is a wrong direction oneway | |
193 | angleAttr.noAccess = true; | |
194 | } | |
195 | ||
196 | // if (node.getCoord().distance(test) < 2){ | |
197 | // if (angleAttr.angle == 20){ | |
198 | // angleAttr.maskedMinAngle = 0x30; | |
199 | // continue; | |
200 | // } | |
201 | // } | |
202 | int sumSpeeds = ag1.maxRoadSpeed + ag2.maxRoadSpeed; | |
203 | if (sumSpeeds <= 1) | |
204 | continue; | |
205 | byte pathAccessMask = (byte) (ag1.orAccessMask & ag2.orAccessMask); | |
206 | if (pathAccessMask == 0){ | |
207 | // no common vehicle allowed on both arcs | |
208 | angleAttr.noAccess = true; | |
209 | } | |
210 | if (angleAttr.noAccess) | |
211 | continue; | |
212 | int maskedMinAngle = MIN_LOW_SPEED_ANGLE; | |
213 | // the Garmin algorithm sees rounded values, so the thresholds are probably | |
214 | // near 22.5 (0x10), 45(0x20), 67.5 (0x30), 90, 112.5 (0x40) | |
215 | ||
216 | // the following code doesn't seem to improve anything, I leave it as comment | |
217 | // for further experiments. | |
218 | // if (cycleMap){ | |
219 | // if (sumSpeeds >= 14) | |
220 | // maskedMinAngle = 0x80; | |
221 | // if (sumSpeeds >= 12) | |
222 | // maskedMinAngle = 0x70; | |
223 | // if (sumSpeeds >= 10) | |
224 | // maskedMinAngle = 0x60; | |
225 | // if (sumSpeeds >= 8) | |
226 | // maskedMinAngle = 0x50; | |
227 | // else if (sumSpeeds >= 6) | |
228 | // maskedMinAngle = 0x40; | |
229 | // else if (sumSpeeds >= 4) | |
230 | // maskedMinAngle = 0x30; | |
231 | // } | |
232 | angleAttr.setMaskedMinAngle(maskedMinAngle); | |
233 | ||
234 | if (angleAttr.maskedDeltaToMin() >= 0) | |
235 | continue; | |
236 | ||
237 | String ignoredReason = null; | |
238 | if (pathAccessMask == AccessTagsAndBits.FOOT) | |
239 | ignoredReason = "because it can only be used by pedestrians"; | |
240 | else if ((pathAccessMask & sharpAnglesCheckMask) == 0) | |
241 | ignoredReason = "because it can not be used by bike"; | |
242 | else if (ag1.isOneway() && ag2.isOneway()){ | |
243 | // both arcs are one-ways, probably the road splits here | |
244 | // to avoid the sharp angles we are looking for | |
245 | ignoredReason = "because it seems to be a flare road"; | |
246 | } | |
247 | else if (ag1.roadDefs.size() == 1 && ag2.roadDefs.size() == 1 && ag1.roadDefs.containsAll(ag2.roadDefs)){ | |
248 | ignoredReason = "because both arcs belong to the same road"; | |
249 | } | |
250 | if (ignoredReason != null){ | |
251 | if (log.isInfoEnabled()){ | |
252 | String sharpAngle = "sharp angle " + angleAttr.angle + "° at " + node.getCoord().toDegreeString(); | |
253 | log.info(sharpAngle, "headings",getCompassBearing(ag1.getInitialHeading()) , getCompassBearing(ag2.getInitialHeading()),"speeds",ag1.maxRoadSpeed, ag2.maxRoadSpeed); | |
254 | log.info("ignoring", sharpAngle, ignoredReason); | |
255 | } | |
256 | angleAttr.setMaskedMinAngle(MIN_ANGLE); | |
257 | angleAttr.noAccess = true; | |
258 | } | |
259 | } | |
260 | ||
261 | for (int i = 0; i < n; i++){ | |
262 | AngleAttr aa = angles[i]; | |
263 | if (aa.maskedAngle >= aa.maskedMinAngle || aa.noAccess) | |
264 | continue; | |
265 | int oldAngle = aa.angle; | |
266 | ArcGroup ag1 = arcGroups.get(i); | |
267 | ArcGroup ag2 = arcGroups.get(i+1 < n ? i+1 : 0); | |
268 | String sharpAngle = ""; | |
269 | if (log.isInfoEnabled()){ | |
270 | sharpAngle = "sharp angle " + aa.angle + "° at " + node.getCoord().toDegreeString(); | |
271 | log.info(sharpAngle, "headings",getCompassBearing(ag1.getInitialHeading()) , getCompassBearing(ag2.getInitialHeading()),"speeds",ag1.maxRoadSpeed, ag2.maxRoadSpeed); | |
272 | } | |
273 | ||
274 | // XXX restrictions ? | |
275 | boolean fixed = false; | |
276 | int wantedIncrement = Math.abs(aa.maskedDeltaToMin()) ; | |
277 | AngleAttr predAA = angles[i == 0 ? n - 1 : i - 1]; | |
278 | AngleAttr nextAA = angles[i >= n - 1 ? 0 : i + 1]; | |
279 | ||
280 | // we can increase the angle by changing the heading values of one or both arcs | |
281 | // find out which one to change first | |
282 | byte origImgDir1 = ag1.imgHeading; | |
283 | byte origImgDir2 = ag2.imgHeading; | |
284 | int origImgAngle = getImgAngle(ag1.imgHeading, ag2.imgHeading); | |
285 | ||
286 | int deltaPred = predAA.maskedDeltaToMin(); | |
287 | int deltaNext = nextAA.maskedDeltaToMin(); | |
288 | ||
289 | if (deltaNext > 0 && (deltaNext > deltaPred || deltaPred < wantedIncrement)){ | |
290 | int usedIncrement = Math.min(wantedIncrement, deltaNext); | |
291 | float oldIH = ag2.getInitialHeading(); | |
292 | int modIH = ag2.imgHeading + usedIncrement; | |
293 | if (modIH > 128) | |
294 | modIH -= 256; | |
295 | ag2.setInitialHeading(modIH * 360/256); | |
296 | int modAngle = Math.round(ag2.getInitialHeading() - ag1.getInitialHeading()); | |
297 | if (modAngle < 0) | |
298 | modAngle += 360; | |
299 | int modImgAngle = getImgAngle(ag1.imgHeading, ag2.imgHeading); | |
300 | if (modImgAngle >= aa.maskedMinAngle) | |
301 | fixed = true; | |
302 | log.info(sharpAngle, "changing arc with heading", getCompassBearing(oldIH), "->",getCompassBearing(ag2.getInitialHeading()), | |
303 | "angle is now",modAngle+"°, in img format:",origImgDir2,"->",ag2.imgHeading, "img angle (0-255)",origImgAngle, "->", modImgAngle); | |
304 | aa.angle = modAngle; | |
305 | nextAA.angle -= usedIncrement; | |
306 | } | |
307 | if (!fixed && deltaPred > 0){ | |
308 | wantedIncrement = Math.abs(aa.maskedDeltaToMin()); | |
309 | int usedIncrement = Math.min(wantedIncrement, deltaPred); | |
310 | float oldIH = ag1.getInitialHeading(); | |
311 | int modIH = ag1.imgHeading - usedIncrement; | |
312 | if (modIH < -128) | |
313 | modIH += 256; | |
314 | ag1.setInitialHeading(modIH * 360/256); | |
315 | int modAngle = Math.round(ag2.getInitialHeading() - ag1.getInitialHeading()); | |
316 | if (modAngle < 0) | |
317 | modAngle += 360; | |
318 | int modImgAngle = getImgAngle(ag1.imgHeading, ag2.imgHeading); | |
319 | if (modImgAngle >= aa.maskedMinAngle) | |
320 | fixed = true; | |
321 | ||
322 | log.info(sharpAngle, "changing arc with heading", getCompassBearing(oldIH), "->", getCompassBearing(ag1.getInitialHeading()), | |
323 | "angle is now",modAngle+"°, in img format:",origImgDir1,"->",ag1.imgHeading, "img angle (0-255)",origImgAngle, "->", modImgAngle); | |
324 | aa.angle = modAngle; | |
325 | predAA.angle -= usedIncrement; | |
326 | } | |
327 | if (!fixed){ | |
328 | if (aa.angle == oldAngle) | |
329 | log.info(sharpAngle, "don't know how to fix it"); | |
330 | else | |
331 | log.info(sharpAngle, "don't know how to enlarge it further"); | |
332 | } | |
333 | } | |
334 | return; | |
335 | } | |
336 | ||
337 | ||
338 | /** | |
339 | * Combine arcs with nearly the same initial heading. | |
340 | * @param node | |
341 | * @return | |
342 | */ | |
343 | private List<ArcGroup> buildArcGroups(RouteNode node) { | |
344 | List<ArcGroup> arcGroups = new ArrayList<>(); | |
345 | List<RouteArc> directArcs = new ArrayList<>(); | |
346 | for (RouteArc arc : node.getArcs()){ | |
347 | if (arc.isDirect()){ | |
348 | directArcs.add(arc); | |
349 | } | |
350 | } | |
351 | if (directArcs.size() < 2) | |
352 | return arcGroups; // should not happen | |
353 | ||
354 | // sort the arcs by initial heading | |
355 | Collections.sort(directArcs, new Comparator<RouteArc>() { | |
356 | public int compare(RouteArc ra1, RouteArc ra2) { | |
357 | if (ra1.getInitialHeading() < ra2.getInitialHeading()) | |
358 | return -1; | |
359 | if (ra1.getInitialHeading() > ra2.getInitialHeading()) | |
360 | return 1; | |
361 | int d = Integer.compare(ra1.getPointsHash(), ra2.getPointsHash()); | |
362 | if (d != 0) | |
363 | return d; | |
364 | d = Long.compare(ra1.getRoadDef().getId() , ra2.getRoadDef().getId()); | |
365 | if (d != 0) | |
366 | return d; | |
367 | return d; | |
368 | } | |
369 | }); | |
370 | ||
371 | Iterator<RouteArc> iter = directArcs.listIterator(); | |
372 | RouteArc arc1 = iter.next(); | |
373 | boolean addArc1 = false; | |
374 | while (iter.hasNext() || addArc1){ | |
375 | ArcGroup ag = new ArcGroup(); | |
376 | ag.initialHeading = arc1.getInitialHeading(); | |
377 | ag.addArc(arc1); | |
378 | arcGroups.add(ag); | |
379 | addArc1 = false; | |
380 | while (iter.hasNext()){ | |
381 | RouteArc arc2 = iter.next(); | |
382 | if (Math.abs(arc1.getInitialHeading()- arc2.getInitialHeading()) < 1){ | |
383 | if (arc1.getDest() != arc2.getDest() && arc1.getRoadDef().getId() != arc2.getRoadDef().getId()) | |
384 | log.warn("sharp angle < 1° at",node.getCoord().toDegreeString(),",maybe duplicated OSM way with bearing",getCompassBearing(arc1.getInitialHeading())); | |
385 | ag.addArc(arc2); | |
386 | } else{ | |
387 | arc1 = arc2; | |
388 | if (iter.hasNext() == false) | |
389 | addArc1 = true; | |
390 | break; | |
391 | } | |
392 | } | |
393 | } | |
394 | for (ArcGroup ag : arcGroups){ | |
395 | ag.imgHeading = (byte) (RouteArc.directionFromDegrees(ag.initialHeading) & mask); | |
396 | } | |
397 | return arcGroups; | |
398 | } | |
399 | ||
400 | /** | |
401 | * for log messages | |
402 | */ | |
403 | private String getCompassBearing (float bearing){ | |
404 | float cb = (bearing + 360) % 360; | |
405 | return Math.round(cb) + "°"; | |
406 | } | |
407 | ||
408 | /** | |
409 | * Debugging aid: guess what angle the Garmin algorithm is using. | |
410 | * @param heading1 | |
411 | * @param heading2 | |
412 | * @return | |
413 | */ | |
414 | private int getImgAngle(byte heading1, byte heading2){ | |
415 | int angle = heading2 - heading1; | |
416 | if (angle < 0) | |
417 | angle += 256; | |
418 | if (angle > 255) | |
419 | angle -= 256; | |
420 | return angle; | |
421 | } | |
422 | ||
423 | } |
48 | 48 | private final List<RouteNode> boundary = new ArrayList<>(); |
49 | 49 | private final List<RoadDef> roadDefs = new ArrayList<>(); |
50 | 50 | private List<RouteCenter> centers = new ArrayList<>(); |
51 | private int adjustTurnHeadings ; | |
51 | private AngleChecker angleChecker = new AngleChecker(); | |
52 | ||
52 | 53 | private boolean checkRoundabouts; |
53 | 54 | private boolean checkRoundaboutFlares; |
54 | 55 | private int maxFlareLengthRatio ; |
55 | 56 | private boolean reportSimilarArcs; |
56 | 57 | |
57 | 58 | public void config(EnhancedProperties props) { |
58 | String ath = props.getProperty("adjust-turn-headings"); | |
59 | if(ath != null) { | |
60 | if(ath.length() > 0) | |
61 | adjustTurnHeadings = Integer.decode(ath); | |
62 | else | |
63 | adjustTurnHeadings = RouteNode.ATH_DEFAULT_MASK; | |
64 | } | |
65 | 59 | checkRoundabouts = props.getProperty("check-roundabouts", false); |
66 | 60 | checkRoundaboutFlares = props.getProperty("check-roundabout-flares", false); |
67 | 61 | maxFlareLengthRatio = props.getProperty("max-flare-length-ratio", 0); |
68 | ||
69 | 62 | reportSimilarArcs = props.getProperty("report-similar-arcs", false); |
63 | angleChecker.config(props); | |
70 | 64 | } |
71 | 65 | |
72 | 66 | public void addRoad(RoadDef roadDef, List<Coord> coordList) { |
127 | 121 | |
128 | 122 | RouteNode node1 = getOrAddNode(lastId, lastCoord); |
129 | 123 | RouteNode node2 = getOrAddNode(id, co); |
130 | ||
131 | 124 | if(node1 == node2) |
132 | 125 | log.error("Road " + roadDef + " contains consecutive identical nodes at " + co.toOSMURL() + " - routing will be broken"); |
133 | 126 | else if(arcLength == 0) |
134 | 127 | log.warn("Road " + roadDef + " contains zero length arc at " + co.toOSMURL()); |
135 | 128 | |
136 | 129 | Coord forwardBearingPoint = coordList.get(lastIndex + 1); |
137 | if(lastCoord.equals(forwardBearingPoint)) { | |
130 | if(lastCoord.equals(forwardBearingPoint) || forwardBearingPoint.isAddedNumberNode()) { | |
138 | 131 | // bearing point is too close to last node to be |
139 | 132 | // useful - try some more points |
140 | 133 | for(int bi = lastIndex + 2; bi <= index; ++bi) { |
141 | if(!lastCoord.equals(coordList.get(bi))) { | |
142 | forwardBearingPoint = coordList.get(bi); | |
143 | break; | |
144 | } | |
134 | Coord coTest = coordList.get(bi); | |
135 | if (coTest.isAddedNumberNode() || lastCoord.equals(coTest)) | |
136 | continue; | |
137 | forwardBearingPoint = coTest; | |
138 | break; | |
145 | 139 | } |
146 | 140 | } |
147 | 141 | Coord reverseBearingPoint = coordList.get(index - 1); |
148 | if(co.equals(reverseBearingPoint)) { | |
142 | if(co.equals(reverseBearingPoint) || reverseBearingPoint.isAddedNumberNode()) { | |
149 | 143 | // bearing point is too close to this node to be |
150 | 144 | // useful - try some more points |
151 | 145 | for(int bi = index - 2; bi >= lastIndex; --bi) { |
152 | if(!co.equals(coordList.get(bi))) { | |
153 | reverseBearingPoint = coordList.get(bi); | |
154 | break; | |
155 | } | |
146 | Coord coTest = coordList.get(bi); | |
147 | if (coTest.isAddedNumberNode() || co.equals(coTest)) | |
148 | continue; | |
149 | reverseBearingPoint = coTest; | |
150 | break; | |
156 | 151 | } |
157 | 152 | } |
158 | 153 | |
161 | 156 | |
162 | 157 | double reverseInitialBearing = co.bearingTo(reverseBearingPoint); |
163 | 158 | double directLength = (lastIndex + 1 == index) ? arcLength : lastCoord.distance(co); |
164 | double reverseFinalBearing, forwardFinalBearing, reverseDirectBearing; | |
159 | double reverseDirectBearing = 0; | |
165 | 160 | if (directLength > 0){ |
166 | 161 | // bearing on rhumb line is a constant, so we can simply revert |
167 | 162 | reverseDirectBearing = (forwardDirectBearing <= 0) ? 180 + forwardDirectBearing: -(180 - forwardDirectBearing) % 180.0; |
168 | forwardFinalBearing = (reverseInitialBearing <= 0) ? 180 + reverseInitialBearing : -(180 - reverseInitialBearing) % 180.0; | |
169 | reverseFinalBearing = (forwardInitialBearing <= 0) ? 180 + forwardInitialBearing : -(180 - forwardInitialBearing) % 180.0; | |
170 | } | |
171 | else { | |
172 | reverseDirectBearing = 0; | |
173 | forwardFinalBearing = 0; | |
174 | reverseFinalBearing = 0; | |
175 | 163 | } |
176 | 164 | // Create forward arc from node1 to node2 |
177 | 165 | RouteArc arc = new RouteArc(roadDef, |
178 | 166 | node1, |
179 | 167 | node2, |
180 | 168 | forwardInitialBearing, |
181 | forwardFinalBearing, | |
182 | 169 | forwardDirectBearing, |
183 | 170 | arcLength, |
184 | 171 | arcLength, |
191 | 178 | RouteArc reverseArc = new RouteArc(roadDef, |
192 | 179 | node2, node1, |
193 | 180 | reverseInitialBearing, |
194 | reverseFinalBearing, | |
195 | 181 | reverseDirectBearing, |
196 | 182 | arcLength, |
197 | 183 | arcLength, |
268 | 254 | if(reportSimilarArcs) |
269 | 255 | node.reportSimilarArcs(); |
270 | 256 | } |
271 | if(adjustTurnHeadings != 0) | |
272 | node.tweezeArcs(adjustTurnHeadings); | |
257 | ||
273 | 258 | nod1.addNode(node); |
274 | 259 | n++; |
275 | 260 | } |
280 | 265 | |
281 | 266 | public List<RouteCenter> getCenters() { |
282 | 267 | if (centers.isEmpty()){ |
268 | angleChecker.check(nodes); | |
283 | 269 | addArcsToMajorRoads(); |
284 | 270 | splitCenters(); |
285 | 271 | } |
42 | 42 | |
43 | 43 | // heading / bearing: |
44 | 44 | private float initialHeading; // degrees (A-> B in an arc ABCD) |
45 | private final float finalHeading; // degrees (C-> D in an arc ABCD) | |
46 | 45 | private final float directHeading; // degrees (A-> D in an arc ABCD) |
47 | 46 | |
48 | 47 | private final RoadDef roadDef; |
80 | 79 | * @param source The source node. (A) |
81 | 80 | * @param dest The destination node (E). |
82 | 81 | * @param initialBearing The initial heading (signed degrees) (A->B) |
83 | * @param finalBearing The final heading (signed degrees) (D->E) | |
84 | 82 | * @param directBearing The direct heading (signed degrees) (A->E) |
85 | 83 | * @param arcLength the length of the arc in meter (A->B->C->D->E) |
86 | 84 | * @param pathLength the length of the arc in meter (summed length for additional arcs) |
89 | 87 | */ |
90 | 88 | public RouteArc(RoadDef roadDef, |
91 | 89 | RouteNode source, RouteNode dest, |
92 | double initialBearing, double finalBearing, double directBearing, | |
90 | double initialBearing, double directBearing, | |
93 | 91 | double arcLength, |
94 | 92 | double pathLength, |
95 | 93 | double directLength, |
99 | 97 | this.source = source; |
100 | 98 | this.dest = dest; |
101 | 99 | this.initialHeading = (float) initialBearing; |
102 | this.finalHeading = (float) finalBearing; | |
103 | 100 | this.directHeading = (directBearing < 180) ? (float) directBearing : -180.0f; |
104 | 101 | int len = NODHeader.metersToRaw(arcLength); |
105 | 102 | if (len >= (1 << 22)) { |
128 | 125 | return initialHeading; |
129 | 126 | } |
130 | 127 | |
128 | public float getDirectHeading() { | |
129 | return directHeading; | |
130 | } | |
131 | ||
131 | 132 | public void setInitialHeading(float ih) { |
132 | 133 | initialHeading = ih; |
133 | 134 | } |
134 | 135 | |
135 | 136 | public float getFinalHeading() { |
136 | return finalHeading; | |
137 | float fh = 0; | |
138 | if (lengthInMeter != 0){ | |
139 | fh = getReverseArc().getInitialHeading(); | |
140 | fh = (fh <= 0) ? 180.0f + fh : -(180.0f - fh) % 180.0f; | |
141 | } | |
142 | return fh; | |
137 | 143 | } |
138 | 144 | |
139 | 145 | public RouteNode getSource() { |
223 | 229 | return lengthInMeter; |
224 | 230 | } |
225 | 231 | |
226 | public static byte directionFromDegrees(double dir) { | |
232 | public static byte directionFromDegrees(float dir) { | |
227 | 233 | return (byte) Math.round(dir * 256.0 / 360) ; |
228 | 234 | } |
229 | 235 |
16 | 16 | import it.unimi.dsi.fastutil.ints.IntArrayList; |
17 | 17 | import java.util.ArrayList; |
18 | 18 | import java.util.Collections; |
19 | import java.util.Comparator; | |
20 | 19 | import java.util.HashSet; |
21 | 20 | import java.util.Iterator; |
22 | 21 | import java.util.List; |
23 | 22 | import uk.me.parabola.imgfmt.app.Coord; |
24 | 23 | import uk.me.parabola.imgfmt.app.CoordNode; |
25 | 24 | import uk.me.parabola.imgfmt.app.ImgFileWriter; |
26 | import uk.me.parabola.imgfmt.app.Label; | |
27 | 25 | import uk.me.parabola.log.Logger; |
28 | 26 | |
29 | 27 | /** |
51 | 49 | private static final int F_ARCS = 0x40; |
52 | 50 | // only used internally in mkgmap |
53 | 51 | private static final int F_DISCARDED = 0x100; // node has been discarded |
54 | ||
55 | private static final int MAX_MAIN_ROAD_HEADING_CHANGE = 120; | |
56 | private static final int MIN_DIFF_BETWEEN_OUTGOING_AND_OTHER_ARCS = 45; | |
57 | private static final int MIN_DIFF_BETWEEN_INCOMING_AND_OTHER_ARCS = 50; | |
58 | 52 | |
59 | 53 | private int offsetNod1 = -1; |
60 | 54 | |
349 | 343 | */ |
350 | 344 | public int compareTo(RouteNode otherNode) { |
351 | 345 | return coord.compareTo(otherNode.getCoord()); |
352 | } | |
353 | ||
354 | private static boolean possiblySameRoad(RouteArc raa, RouteArc rab) { | |
355 | ||
356 | RoadDef rda = raa.getRoadDef(); | |
357 | RoadDef rdb = rab.getRoadDef(); | |
358 | ||
359 | if(rda.getId() == rdb.getId()) { | |
360 | // roads have the same (OSM) id | |
361 | return true; | |
362 | } | |
363 | ||
364 | boolean bothArcsNamed = false; | |
365 | for(Label laba : rda.getLabels()) { | |
366 | if(laba != null && laba.getOffset() != 0) { | |
367 | for(Label labb : rdb.getLabels()) { | |
368 | if(labb != null && labb.getOffset() != 0) { | |
369 | bothArcsNamed = true; | |
370 | if(laba.equals(labb)) { | |
371 | // the roads have the same label | |
372 | if(rda.isLinkRoad() == rdb.isLinkRoad()) { | |
373 | // if both are a link road or both are | |
374 | // not a link road, consider them the | |
375 | // same road | |
376 | return true; | |
377 | } | |
378 | // One is a link road and the other isn't | |
379 | // so consider them different roads - this | |
380 | // is because people often give a link | |
381 | // road that's leaving some road the same | |
382 | // ref as that road but it suits us better | |
383 | // to consider them as different roads | |
384 | return false; | |
385 | } | |
386 | } | |
387 | } | |
388 | } | |
389 | } | |
390 | ||
391 | if(bothArcsNamed) { | |
392 | // both roads have names and they don't match | |
393 | return false; | |
394 | } | |
395 | ||
396 | // at least one road is unnamed | |
397 | if(rda.isRoundabout() && rdb.isRoundabout()) { | |
398 | // hopefully, segments of the same (unnamed) roundabout | |
399 | return true; | |
400 | } | |
401 | ||
402 | return false; | |
403 | } | |
404 | ||
405 | private static boolean rightTurnRequired(float inHeading, float outHeading, float otherHeading) { | |
406 | // given the headings of the incoming, outgoing and side | |
407 | // roads, decide whether a side road is to the left or the | |
408 | // right of the main road | |
409 | ||
410 | outHeading -= inHeading; | |
411 | while(outHeading < -180) | |
412 | outHeading += 360; | |
413 | while(outHeading > 180) | |
414 | outHeading -= 360; | |
415 | ||
416 | otherHeading -= inHeading; | |
417 | while(otherHeading < -180) | |
418 | otherHeading += 360; | |
419 | while(otherHeading > 180) | |
420 | otherHeading -= 360; | |
421 | ||
422 | return otherHeading > outHeading; | |
423 | } | |
424 | ||
425 | private static final int ATH_OUTGOING = 1; | |
426 | private static final int ATH_INCOMING = 2; | |
427 | ||
428 | public static final int ATH_DEFAULT_MASK = ATH_OUTGOING | ATH_INCOMING; | |
429 | ||
430 | public void tweezeArcs(int mask) { | |
431 | if(arcs.size() >= 3) { | |
432 | ||
433 | // detect the "shallow turn" scenario where at a junction | |
434 | // on some "main" road, the side road leaves the main | |
435 | // road at a very shallow angle and the GPS says "keep | |
436 | // right/left" when it would be better if it said "turn | |
437 | // right/left" | |
438 | ||
439 | // also helps to produce a turn instruction when the main | |
440 | // road bends sharply but the side road keeps close to the | |
441 | // original heading | |
442 | ||
443 | // the code tries to detect a pair of arcs (the "incoming" | |
444 | // arc and the "outgoing" arc) that are the "main road" | |
445 | // and the remaining arc (called the "other" arc) which is | |
446 | // the "side road" | |
447 | ||
448 | // having worked out the roles for the arcs, the heuristic | |
449 | // applied is that if the main road doesn't change its | |
450 | // heading by more than maxMainRoadHeadingChange, ensure | |
451 | // that the side road heading differs from the outgoing | |
452 | // heading by at least | |
453 | // minDiffBetweenOutgoingAndOtherArcs and the side road | |
454 | // heading differs from the incoming heading by at least | |
455 | // minDiffBetweenIncomingAndOtherArcs | |
456 | ||
457 | // list of outgoing arcs discovered at this node | |
458 | List<RouteArc> outgoingArcs = new ArrayList<RouteArc>(); | |
459 | ||
460 | // sort incoming arcs by decreasing class/speed | |
461 | List<RouteArc> inArcs = new ArrayList<RouteArc>(); | |
462 | for (RouteArc arc : arcs){ | |
463 | if (arc.isDirect()) | |
464 | inArcs.add(arc.getReverseArc()); | |
465 | } | |
466 | ||
467 | Collections.sort(inArcs, new Comparator<RouteArc>() { | |
468 | public int compare(RouteArc ra1, RouteArc ra2) { | |
469 | int c1 = ra1.getRoadDef().getRoadClass(); | |
470 | int c2 = ra2.getRoadDef().getRoadClass(); | |
471 | if(c1 == c2) | |
472 | return (ra2.getRoadDef().getRoadSpeed() - | |
473 | ra1.getRoadDef().getRoadSpeed()); | |
474 | return c2 - c1; | |
475 | } | |
476 | }); | |
477 | ||
478 | // look at incoming arcs in order of decreasing class/speed | |
479 | for(RouteArc inArc : inArcs) { | |
480 | ||
481 | RoadDef inRoadDef = inArc.getRoadDef(); | |
482 | ||
483 | if(!inArc.isForward() && inRoadDef.isOneway()) { | |
484 | // ignore reverse arc if road is oneway | |
485 | continue; | |
486 | } | |
487 | ||
488 | float inHeading = inArc.getFinalHeading(); | |
489 | // determine the outgoing arc that is likely to be the | |
490 | // same road as the incoming arc | |
491 | RouteArc outArc = null; | |
492 | ||
493 | if(throughRoutes != null) { | |
494 | // through_route relations have the highest precedence | |
495 | for(RouteArc[] pair : throughRoutes) { | |
496 | if(pair[0] == inArc) { | |
497 | outArc = pair[1]; | |
498 | log.info("Found through route from " + inRoadDef + " to " + outArc.getRoadDef()); | |
499 | break; | |
500 | } | |
501 | } | |
502 | } | |
503 | ||
504 | if(outArc == null) { | |
505 | // next, if oa has the same RoadDef as inArc, it's | |
506 | // definitely the same road | |
507 | for(RouteArc oa : arcs) { | |
508 | if (oa.isDirect() == false) | |
509 | continue; | |
510 | if(oa.getDest() != inArc.getSource()) { | |
511 | // this arc is not going to the same node as | |
512 | // inArc came from | |
513 | if(oa.getRoadDef() == inRoadDef) { | |
514 | outArc = oa; | |
515 | break; | |
516 | } | |
517 | } | |
518 | } | |
519 | } | |
520 | ||
521 | if(outArc == null) { | |
522 | // next, although the RoadDefs don't match, use | |
523 | // possiblySameRoad() to see if the roads' id or | |
524 | // labels (names/refs) match | |
525 | for(RouteArc oa : arcs) { | |
526 | if (oa.isDirect() == false) | |
527 | continue; | |
528 | if(oa.getDest() != inArc.getSource()) { | |
529 | // this arc is not going to the same node as | |
530 | // inArc came from | |
531 | if((oa.isForward() || !oa.getRoadDef().isOneway()) && | |
532 | possiblySameRoad(inArc, oa)) { | |
533 | outArc = oa; | |
534 | break; | |
535 | } | |
536 | } | |
537 | } | |
538 | } | |
539 | ||
540 | // if we did not find the outgoing arc, give up with | |
541 | // this incoming arc | |
542 | if(outArc == null) { | |
543 | //log.info("Can't continue road " + inRoadDef + " at " + coord.toOSMURL()); | |
544 | continue; | |
545 | } | |
546 | ||
547 | // remember that this arc is an outgoing arc | |
548 | outgoingArcs.add(outArc); | |
549 | ||
550 | float outHeading = outArc.getInitialHeading(); | |
551 | float mainHeadingDelta = outHeading - inHeading; | |
552 | while(mainHeadingDelta > 180) | |
553 | mainHeadingDelta -= 360; | |
554 | while(mainHeadingDelta < -180) | |
555 | mainHeadingDelta += 360; | |
556 | //log.info(inRoadDef + " continues to " + outArc.getRoadDef() + " with a heading change of " + mainHeadingDelta + " at " + coord.toOSMURL()); | |
557 | ||
558 | if(Math.abs(mainHeadingDelta) > MAX_MAIN_ROAD_HEADING_CHANGE) { | |
559 | // if the continuation road heading change is | |
560 | // greater than maxMainRoadHeadingChange don't | |
561 | // adjust anything | |
562 | continue; | |
563 | } | |
564 | ||
565 | for(RouteArc otherArc : arcs) { | |
566 | if (otherArc.isDirect() == false) | |
567 | continue; | |
568 | ||
569 | // for each other arc leaving this node, tweeze | |
570 | // its heading if its heading change from the | |
571 | // outgoing heading is less than | |
572 | // minDiffBetweenOutgoingAndOtherArcs or its | |
573 | // heading change from the incoming heading is | |
574 | // less than minDiffBetweenIncomingAndOtherArcs | |
575 | ||
576 | if(otherArc.getDest() == inArc.getSource() || | |
577 | otherArc == outArc) { | |
578 | // we're looking at the incoming or outgoing | |
579 | // arc, ignore it | |
580 | continue; | |
581 | } | |
582 | ||
583 | if(!otherArc.isForward() && | |
584 | otherArc.getRoadDef().isOneway()) { | |
585 | // ignore reverse arc if road is oneway | |
586 | continue; | |
587 | } | |
588 | ||
589 | if(inRoadDef.isLinkRoad() && | |
590 | otherArc.getRoadDef().isLinkRoad()) { | |
591 | // it's a link road leaving a link road so | |
592 | // leave the angle unchanged to avoid | |
593 | // introducing a time penalty by increasing | |
594 | // the angle (this stops the router using link | |
595 | // roads that "cut the corner" at roundabouts) | |
596 | continue; | |
597 | } | |
598 | ||
599 | if(outgoingArcs.contains(otherArc)) { | |
600 | // this arc was previously matched as an | |
601 | // outgoing arc so we don't want to change its | |
602 | // heading now | |
603 | continue; | |
604 | } | |
605 | ||
606 | float otherHeading = otherArc.getInitialHeading(); | |
607 | float outToOtherDelta = otherHeading - outHeading; | |
608 | while(outToOtherDelta > 180) | |
609 | outToOtherDelta -= 360; | |
610 | while(outToOtherDelta < -180) | |
611 | outToOtherDelta += 360; | |
612 | float inToOtherDelta = otherHeading - inHeading; | |
613 | while(inToOtherDelta > 180) | |
614 | inToOtherDelta -= 360; | |
615 | while(inToOtherDelta < -180) | |
616 | inToOtherDelta += 360; | |
617 | ||
618 | float newHeading = otherHeading; | |
619 | if(rightTurnRequired(inHeading, outHeading, otherHeading)) { | |
620 | // side road to the right | |
621 | if((mask & ATH_OUTGOING) != 0 && | |
622 | Math.abs(outToOtherDelta) < MIN_DIFF_BETWEEN_OUTGOING_AND_OTHER_ARCS) | |
623 | newHeading = outHeading + MIN_DIFF_BETWEEN_OUTGOING_AND_OTHER_ARCS; | |
624 | if((mask & ATH_INCOMING) != 0 && | |
625 | Math.abs(inToOtherDelta) < MIN_DIFF_BETWEEN_INCOMING_AND_OTHER_ARCS) { | |
626 | float nh = inHeading + MIN_DIFF_BETWEEN_INCOMING_AND_OTHER_ARCS; | |
627 | if(nh > newHeading) | |
628 | newHeading = nh; | |
629 | } | |
630 | ||
631 | if(newHeading > 180) | |
632 | newHeading -= 360; | |
633 | } | |
634 | else { | |
635 | // side road to the left | |
636 | if((mask & ATH_OUTGOING) != 0 && | |
637 | Math.abs(outToOtherDelta) < MIN_DIFF_BETWEEN_OUTGOING_AND_OTHER_ARCS) | |
638 | newHeading = outHeading - MIN_DIFF_BETWEEN_OUTGOING_AND_OTHER_ARCS; | |
639 | if((mask & ATH_INCOMING) != 0 && | |
640 | Math.abs(inToOtherDelta) < MIN_DIFF_BETWEEN_INCOMING_AND_OTHER_ARCS) { | |
641 | float nh = inHeading - MIN_DIFF_BETWEEN_INCOMING_AND_OTHER_ARCS; | |
642 | if(nh < newHeading) | |
643 | newHeading = nh; | |
644 | } | |
645 | ||
646 | if(newHeading < -180) | |
647 | newHeading += 360; | |
648 | } | |
649 | if(Math.abs(newHeading - otherHeading) > 0.0000001) { | |
650 | otherArc.setInitialHeading(newHeading); | |
651 | log.info("Adjusting turn heading from " + otherHeading + " to " + newHeading + " at junction of " + inRoadDef + " and " + otherArc.getRoadDef() + " at " + coord.toOSMURL()); | |
652 | } | |
653 | } | |
654 | } | |
655 | } | |
656 | 346 | } |
657 | 347 | |
658 | 348 | public void checkRoundabouts() { |
1009 | 699 | sourceNode, |
1010 | 700 | destNode, |
1011 | 701 | roadArcs.get(i).getInitialHeading(), // not used |
1012 | arcToDest.getFinalHeading(), // not used | |
1013 | 702 | c1.bearingTo(c2), |
1014 | 703 | partialArcLength, // from stepNode to destNode on road |
1015 | 704 | pathLength, // from sourceNode to destNode on road |
1110 | 799 | public int hashCode(){ |
1111 | 800 | return getCoord().getId(); |
1112 | 801 | } |
802 | ||
803 | public List<RouteArc> getDirectArcsBetween(RouteNode otherNode) { | |
804 | List<RouteArc> result = new ArrayList<>(); | |
805 | for(RouteArc a : arcs){ | |
806 | if(a.isDirect() && a.getDest() == otherNode){ | |
807 | result.add(a); | |
808 | } | |
809 | } | |
810 | return result; | |
811 | } | |
812 | ||
813 | ||
1113 | 814 | } |
1069 | 1069 | */ |
1070 | 1070 | private int addAsNumberNode(int pos, Coord toAdd){ |
1071 | 1071 | toAdd.setNumberNode(true); |
1072 | toAdd.setAddedNumberNode(true); | |
1072 | 1073 | getRoad().getPoints().add(pos, toAdd); |
1073 | 1074 | |
1074 | 1075 | ExtNumbers work = next; |
907 | 907 | MapRoad uncheckedRoads[] = new MapRoad[houses.length]; |
908 | 908 | for (int i = 0 ; i < houses.length; i++) |
909 | 909 | uncheckedRoads[i] = houses[i].getRoad(); |
910 | ||
911 | 910 | isOK = info.checkRoads(); |
912 | if (!isOK) | |
913 | continue; | |
914 | 911 | // check if houses are assigned to different roads now |
915 | 912 | houses = info.getHouseNodes(); |
916 | 913 | for (int i = 0 ; i < houses.length; i++){ |
918 | 915 | initialHousesForRoads.removeMapping(uncheckedRoads[i], houses[i]); |
919 | 916 | if (houses[i].isIgnored() == false) |
920 | 917 | initialHousesForRoads.add(houses[i].getRoad(), houses[i]); |
918 | else { | |
919 | if (!isOK) | |
920 | log.info("housenumber is assigned to different road after checking addr:interpolation way which turned out to be invalid",houses[i],info ); | |
921 | } | |
921 | 922 | } |
922 | 923 | } |
923 | 924 | } |
163 | 163 | if (timesToAdd == 2){ |
164 | 164 | // add two new points between c1 and c2 |
165 | 165 | points.add(seg + 1, pointToUse); |
166 | pointToUse.setAddedNumberNode(true); | |
166 | 167 | pointToUse = new Coord (pointToUse); |
167 | 168 | pointToUse.setNumberNode(true); |
168 | 169 | points.add(seg + 1, pointToUse); |
170 | pointToUse.setAddedNumberNode(true); | |
169 | 171 | linkNode = pointToUse; |
170 | 172 | } else { |
171 | 173 | // copy it |
173 | 175 | pointToUse.setNumberNode(true); |
174 | 176 | // add copy before c2 |
175 | 177 | points.add(seg + 1, pointToUse); |
178 | pointToUse.setAddedNumberNode(true); | |
176 | 179 | if (pointToUse.highPrecEquals(c1)){ |
177 | 180 | linkNode = c1; |
178 | 181 | } else { |
170 | 170 | while (streetName.equals(knownHouses[i].getRoad().getStreet()) == false && knownHouses[i].hasAlternativeRoad()){ |
171 | 171 | HousenumberMatch testx = new HousenumberMatch(knownHouses[i]); |
172 | 172 | MapRoad r = knownHouses[i].getAlternativeRoads().remove(0); |
173 | HousenumberGenerator.findClosestRoadSegment(testx, r); | |
174 | if (testx.getDistance() < MAX_INTERPOLATION_DISTANCE_TO_ROAD){ | |
175 | copyRoadData(testx, knownHouses[i]); | |
173 | if (streetName.equals(r.getStreet())){ | |
174 | HousenumberGenerator.findClosestRoadSegment(testx, r); | |
175 | if (testx.getDistance() < MAX_INTERPOLATION_DISTANCE_TO_ROAD){ | |
176 | copyRoadData(testx, knownHouses[i]); | |
177 | } | |
176 | 178 | } |
177 | 179 | } |
178 | 180 | } |
18 | 18 | import java.util.Iterator; |
19 | 19 | import java.util.Map; |
20 | 20 | |
21 | import uk.me.parabola.imgfmt.app.Label; | |
22 | import uk.me.parabola.log.Logger; | |
23 | ||
24 | ||
21 | 25 | /** |
22 | 26 | * Superclass of the node, segment and way OSM elements. |
23 | 27 | */ |
24 | 28 | public abstract class Element { |
29 | private static final Logger log = Logger.getLogger(Element.class); | |
30 | ||
25 | 31 | private Tags tags; |
26 | 32 | private long id; |
27 | 33 | private long originalId; |
31 | 37 | } |
32 | 38 | |
33 | 39 | /** |
34 | * Add a tag to the way. Some tags are recognised separately and saved in | |
40 | * Add a tag to the element. This method should be called by OSM readers | |
41 | * because it trims obsolete spaces from the value. | |
42 | * | |
43 | * @param key The tag name. | |
44 | * @param val Its value. | |
45 | */ | |
46 | public void addTagFromRawOSM(String key, String val) { | |
47 | if (val != null){ | |
48 | val = val.trim(); | |
49 | if (val.isEmpty() == false){ | |
50 | // remove duplicated spaces within value | |
51 | String squashed = Label.squashSpaces(val); | |
52 | if (val.equals(squashed) == false) { | |
53 | if (log.isInfoEnabled()) | |
54 | log.info(this.toBrowseURL(),"obsolete blanks removed from tag", key, " '" + val + "' -> '" + squashed + "'"); | |
55 | val = squashed; | |
56 | } | |
57 | } | |
58 | } | |
59 | addTag(key, val.intern()); | |
60 | } | |
61 | ||
62 | /** | |
63 | * Add a tag to the element. Some tags are recognised separately and saved in | |
35 | 64 | * separate fields. |
36 | 65 | * |
37 | 66 | * @param key The tag name. |
44 | 73 | } |
45 | 74 | |
46 | 75 | /** |
47 | * Add a tag to the way. Some tags are recognised separately and saved in | |
76 | * Add a tag to the element. Some tags are recognised separately and saved in | |
48 | 77 | * separate fields. |
49 | 78 | * |
50 | 79 | * @param tagKey The tag id created by TagDict |
69 | 69 | Node node = new Node(id, co); |
70 | 70 | for (int tid = 0; tid < tagCount; tid++) { |
71 | 71 | String key = getStringById(binNode.getKeys(tid)); |
72 | String val = getStringById(binNode.getVals(tid)).trim(); | |
72 | String val = getStringById(binNode.getVals(tid)); | |
73 | 73 | key = keepTag(key, val); |
74 | 74 | if (key != null) |
75 | node.addTag(key, val.intern()); | |
75 | node.addTagFromRawOSM(key, val); | |
76 | 76 | } |
77 | 77 | |
78 | 78 | saver.addNode(node); |
104 | 104 | int keyid = nodes.getKeysVals(kvid++); |
105 | 105 | int valid = nodes.getKeysVals(kvid++); |
106 | 106 | String key = getStringById(keyid); |
107 | String val = getStringById(valid).trim(); | |
107 | String val = getStringById(valid); | |
108 | 108 | key = keepTag(key, val); |
109 | 109 | if (key != null) { |
110 | 110 | if (node == null) |
111 | 111 | node = new Node(id, co); |
112 | node.addTag(key, val.intern()); | |
112 | node.addTagFromRawOSM(key, val); | |
113 | 113 | ntags++; |
114 | 114 | } |
115 | 115 | } |
131 | 131 | for (int j = 0; j < binWay.getKeysCount(); j++) { |
132 | 132 | |
133 | 133 | String key = getStringById(binWay.getKeys(j)); |
134 | String val = getStringById(binWay.getVals(j)).trim(); | |
134 | String val = getStringById(binWay.getVals(j)); | |
135 | 135 | key = keepTag(key, val); |
136 | 136 | if (key != null) |
137 | way.addTag(key, val.intern()); | |
137 | way.addTagFromRawOSM(key, val); | |
138 | 138 | } |
139 | 139 | |
140 | 140 | long nid = 0; |
156 | 156 | boolean tagsIncomplete = false; |
157 | 157 | for (int j = 0; j < binRel.getKeysCount(); j++) { |
158 | 158 | String key = getStringById(binRel.getKeys(j)); |
159 | String val = getStringById(binRel.getVals(j)).trim(); | |
159 | String val = getStringById(binRel.getVals(j)); | |
160 | 160 | // type is required for relations - all other tags are filtered |
161 | 161 | if ("type".equals(key)) |
162 | 162 | // intern the string |
166 | 166 | if (key == null) |
167 | 167 | tagsIncomplete = true; |
168 | 168 | else |
169 | rel.addTag(key, val.intern()); | |
169 | rel.addTagFromRawOSM(key, val); | |
170 | 170 | } |
171 | 171 | |
172 | 172 | if (tagsIncomplete) { |
313 | 313 | while (bytesToRead > 0){ |
314 | 314 | readStringPair(); |
315 | 315 | String key = stringPair[0]; |
316 | String val = stringPair[1].trim(); | |
316 | String val = stringPair[1]; | |
317 | 317 | // the type tag is required for relations - all other tags are filtered |
318 | 318 | if (elem instanceof Relation && "type".equals(key)) |
319 | 319 | // intern the string |
321 | 321 | else |
322 | 322 | key = keepTag(key, val); |
323 | 323 | if (key != null) |
324 | elem.addTag(key, val.intern()); | |
324 | elem.addTagFromRawOSM(key, val); | |
325 | 325 | else |
326 | 326 | tagsIncomplete = true; |
327 | 327 | } |
206 | 206 | private void startInNode(String qName, Attributes attributes) { |
207 | 207 | if (qName.equals("tag")) { |
208 | 208 | String key = attributes.getValue("k"); |
209 | String val = attributes.getValue("v").trim(); | |
209 | String val = attributes.getValue("v"); | |
210 | 210 | |
211 | 211 | // We only want to create a full node for nodes that are POI's |
212 | 212 | // and not just one point of a way. Only create if it has tags that |
218 | 218 | currentNode = new Node(currentElementId, co); |
219 | 219 | } |
220 | 220 | |
221 | currentNode.addTag(key, val.intern()); | |
221 | currentNode.addTagFromRawOSM(key, val); | |
222 | 222 | } |
223 | 223 | } |
224 | 224 | } |
234 | 234 | addCoordToWay(currentWay, id); |
235 | 235 | } else if (qName.equals("tag")) { |
236 | 236 | String key = attributes.getValue("k"); |
237 | String val = attributes.getValue("v").trim(); | |
237 | String val = attributes.getValue("v"); | |
238 | 238 | key = keepTag(key, val); |
239 | 239 | if (key != null) |
240 | currentWay.addTag(key, val.intern()); | |
240 | currentWay.addTagFromRawOSM(key, val); | |
241 | 241 | } |
242 | 242 | } |
243 | 243 | |
275 | 275 | currentRelation.addElement(attributes.getValue("role"), el); |
276 | 276 | } else if (qName.equals("tag")) { |
277 | 277 | String key = attributes.getValue("k"); |
278 | String val = attributes.getValue("v").trim(); | |
278 | String val = attributes.getValue("v"); | |
279 | 279 | // the type tag is required for relations - all other tags are filtered |
280 | 280 | if ("type".equals(key)) |
281 | 281 | // intern the key |
285 | 285 | if (key == null) { |
286 | 286 | currentRelation.addTag(TAGS_INCOMPLETE_TAG, "true"); |
287 | 287 | } else { |
288 | currentRelation.addTag(key, val.intern()); | |
288 | currentRelation.addTagFromRawOSM(key, val); | |
289 | 289 | } |
290 | 290 | } |
291 | 291 | } |
53 | 53 | new String[] {"1", "2", "3"}, |
54 | 54 | values.toArray()); |
55 | 55 | } |
56 | ||
57 | @Test | |
58 | public void testaddTagFromRawOSM() { | |
59 | Element el = new Way(1); | |
60 | ||
61 | el.addTagFromRawOSM("a", "1"); | |
62 | el.addTagFromRawOSM("b", "1 "); | |
63 | el.addTagFromRawOSM("c", " 1"); | |
64 | el.addTagFromRawOSM("d", "1 2"); | |
65 | el.addTagFromRawOSM("e", "1 2 3"); | |
66 | el.addTagFromRawOSM("f", " 1 2 3 4 "); | |
67 | el.addTagFromRawOSM("g", " "); | |
68 | el.addTagFromRawOSM("h", " "); | |
69 | ||
70 | assertEquals("1", el.getTag("a")); | |
71 | assertEquals("1", el.getTag("b")); | |
72 | assertEquals("1", el.getTag("c")); | |
73 | assertEquals("1 2", el.getTag("d")); | |
74 | assertEquals("1 2 3", el.getTag("e")); | |
75 | assertEquals("1 2 3 4", el.getTag("f")); | |
76 | assertEquals("", el.getTag("g")); | |
77 | assertEquals("", el.getTag("h")); | |
78 | } | |
56 | 79 | } |