New upstream version 0.0.0+svn4475
Bas Couwenberg
4 years ago
311 | 311 | ignoreerrors="true"/> |
312 | 312 | <get src="http://www.mkgmap.org.uk/testinput/osm/uk-test-2.osm.gz" |
313 | 313 | dest="test/resources/in/osm/uk-test-2.osm.gz" usetimestamp="true" |
314 | ignoreerrors="true"/> | |
315 | <get src="http://www.mkgmap.org.uk/testinput/osm/is-in-samples.osm" | |
316 | dest="test/resources/in/osm/is-in-samples.osm" usetimestamp="true" | |
314 | 317 | ignoreerrors="true"/> |
315 | 318 | <get src="http://www.mkgmap.org.uk/testinput/mp/test1.mp" |
316 | 319 | dest="test/resources/in/mp/test1.mp" usetimestamp="true" |
18 | 18 | need to use this option to allow more memory to be allocated to the Java |
19 | 19 | heap. Typically, mkgmap requires about 500MB per core, so an 8-core |
20 | 20 | processor might need to specify -Xmx4g - note there is no space or |
21 | equals sign in the option. | |
21 | equals sign in the option. | |
22 | 22 | |
23 | 23 | ;-enableassertions |
24 | ;-ea | |
24 | 25 | : Causes an error to be thrown if an assertion written in the mkgmap |
25 | 26 | code is evaluated as not true. This is useful in detecting bugs in the |
26 | 27 | mkgmap code. |
34 | 35 | === Mkgmap options === |
35 | 36 | |
36 | 37 | The order of the options is significant in that options only apply |
37 | to subsequent input files. If you are using splitter, you probably | |
38 | to subsequent input files. If you are using splitter, you probably | |
38 | 39 | will need to put most of your options before '-c template.args' |
39 | 40 | (this file is generated by splitter). |
40 | 41 | |
46 | 47 | : Display help on the given topic. If the topic is omitted then |
47 | 48 | general help information is displayed, the same as in help=help. |
48 | 49 | |
49 | ;--version | |
50 | ;--version | |
50 | 51 | : Write program version to stderr. |
51 | 52 | |
52 | 53 | === File options === |
54 | 55 | ;filename |
55 | 56 | ;--input-file=filename |
56 | 57 | : Read input data from the given file. This option (or just a |
57 | filename) may be specified more than once. Make sure you set all | |
58 | filename) may be specified more than once. Make sure you set all | |
58 | 59 | wanted options before this. |
59 | 60 | |
60 | 61 | ;--gmapsupp |
78 | 79 | <p> |
79 | 80 | Lines beginning with a # character are ignored and can be used as |
80 | 81 | comments. Any command line option can be specified, however the |
81 | leading '--' must be omitted. The short option names with a single | |
82 | '-' cannot be used, simply use the long name instead. | |
82 | leading '--' must be omitted. | |
83 | The short option names with a single '-' cannot be used, simply use the long name instead. | |
83 | 84 | |
84 | 85 | ;--output-dir=directory |
85 | 86 | : Specify the directory in which all output files are written. It defaults |
143 | 144 | gmapsupp.img file so that address search will work on a GPS |
144 | 145 | device. |
145 | 146 | <p> |
146 | If both the --gmapsupp and any of --tdbfile, --gmapi, or --nsis options | |
147 | If both the --gmapsupp and any of --tdbfile, --gmapi, or --nsis options | |
147 | 148 | are given alongside the --index option, then both indexes will be created. |
148 | 149 | Note that this will require roughly twice as much memory. |
149 | 150 | <p> |
187 | 188 | of special characters in the road labels to mark the beginning and end of |
188 | 189 | the important part. In combination with option --split-name-index |
189 | 190 | only the words in the important part are indexed. |
190 | <p> | |
191 | ||
191 | 192 | :There are two main effects of this option: |
192 | 193 | :: - On the PC, when zooming out, the name 'Rue de la Concorde' is only |
193 | 194 | rendered as 'Concorde'. |
194 | 195 | :: - The index for road names only contains the important part of the name. |
195 | 196 | You can search for road name Conc to find road names like 'Rue de la Concorde'. |
196 | However, a search for 'Rue' will not list 'Rue de la Concorde' or | |
197 | 'Rue du Moulin'. It may list 'Rueben Brookins Road' if that is in the map. | |
197 | However, a search for 'Rue' will not list 'Rue de la Concorde' or 'Rue du Moulin'. | |
198 | It may list 'Rueben Brookins Road' if that is in the map. | |
198 | 199 | Only MapSource shows a corresponding hint. |
199 | <p> | |
200 | :: Another effect is that the index is smaller. | |
200 | ||
201 | :: Another effect is that the index is smaller. | |
201 | 202 | : See comments in the sample roadNameConfig.txt for further details. |
202 | 203 | |
203 | 204 | ;--mdr7-excl=name[,name...] |
220 | 221 | :: - 0x00 .. 0x0f (cities, sub type 0, type <= 0xf) |
221 | 222 | :: - 0x2axx..0x30xx (Food & Drink, Lodging, ...) |
222 | 223 | :: - 0x28xx (no category ?) |
223 | :: - 0x64xx .. 0x66xx (attractions) | |
224 | : This option allows the exclusion of POI types from the index. | |
224 | :: - 0x64xx .. 0x66xx (attractions) | |
225 | : This option allows the exclusion of POI types from the index. | |
225 | 226 | The excluded types are not indexed, but may still be searchable on a device, |
226 | 227 | as some devices seem to ignore most of the index, e.g. an Oregon 600 with |
227 | 228 | firmware 5.00 only seems to use it for city search. |
228 | 229 | If your device finds a POI name like 'Planet' when you search for 'Net', |
229 | 230 | it doesn't use the index because the index created by mkgmap cannot help for |
230 | 231 | that search. |
231 | <p> | |
232 | ||
232 | 233 | : So, this option may help when you care about the size of the index or the |
233 | memory that is needed to calculate it. | |
234 | memory that is needed to calculate it. | |
234 | 235 | The option expects a comma separated list of types or type ranges. A range is |
235 | 236 | given with from-type-to-type, e.g. 0x6400-0x6405. First and last type are both |
236 | excluded. A range can span multiple types, e.g. 0x6400-0x661f. | |
237 | : Examples for usage: | |
237 | excluded. A range can span multiple types, e.g. 0x6400-0x661f. | |
238 | : Examples for usage: | |
238 | 239 | :: - Assume your style adds a POI with type 0x2800 for each addr:housenumber. |
239 | 240 | It is not useful to index those numbers, so you can use --poi-excl-index=0x2800 |
240 | 241 | to exclude this. |
252 | 253 | <p> |
253 | 254 | The following special tags are added: |
254 | 255 | <pre> |
255 | mkgmap:admin_level2 : Name of the admin_level=2 boundary | |
256 | mkgmap:admin_level2 : Name of the admin_level=2 boundary | |
256 | 257 | mkgmap:admin_level3 : Name of the admin_level=3 boundary |
257 | 258 | .. |
258 | 259 | mkgmap:admin_level11 |
264 | 265 | uk.me.parabola.mkgmap.reader.osm.boundary.BoundaryPreprocessor |
265 | 266 | <inputfile> <boundsdir> |
266 | 267 | </pre> |
267 | :The input file must contain the boundaries that should be pre-processed. | |
268 | It can have OSM, PBF or O5M file format. It is recommended that it | |
268 | :The input file must contain the boundaries that should be pre-processed. | |
269 | It can have OSM, PBF or O5M file format. It is recommended that it | |
269 | 270 | contains the boundary data only to avoid very high memory usage. |
270 | 271 | The boundsdir gives the directory where the processed files are stored. |
271 | This directory can be used as --bounds parameter with mkgmap. | |
272 | This directory can be used as --bounds parameter with mkgmap. | |
272 | 273 | |
273 | 274 | ;--location-autofill=[option1,[option2]] |
274 | : Controls how the address fields for country, region, city and zip info | |
275 | : Controls how the address fields for country, region, city and zip info | |
275 | 276 | are gathered automatically if the fields are not set by using the special |
276 | 277 | mkgmap address tags (e.g. mkgmap:city - see option index). |
277 | 278 | Warning: automatic assignment of address fields is somehow a best guess. |
278 | 279 | :;is_in |
279 | 280 | :: The is_in tag is analysed for country and region information. |
280 | <p> | |
281 | 281 | :;nearest |
282 | :: The city/hamlet points that are closest to the element are used | |
283 | to assign the missing address fields. Beware that cities located | |
284 | in the same tile are used only. So the results close to a tile | |
285 | border have less quality. | |
282 | :: The city/hamlet points that are closest to the element are used | |
283 | to assign the missing address fields. Beware that cities located | |
284 | in the same tile are used only. So the results close to a tile | |
285 | border have less quality. | |
286 | 286 | |
287 | 287 | ;--housenumbers |
288 | : Enables house number search for OSM input files. | |
289 | All nodes and polygons having addr:housenumber set are matched | |
288 | : Enables house number search for OSM input files. | |
289 | All nodes and polygons having addr:housenumber set are matched | |
290 | 290 | to streets. A match between a house number element and a street is created if |
291 | 291 | the street is located within a radius of 150m and the addr:street tag value of |
292 | 292 | the house number element equals the mgkmap:street tag value of the street. |
293 | 293 | The mkgmap:street tag must be added to the street in the style file. |
294 | 294 | For optimal results, the tags mkgmap:city and mkgmap:postal_code should be |
295 | 295 | set for the housenumber element. If a street connects two or more cities |
296 | this allows all addresses along the road to be found, even when they have the | |
296 | this allows all addresses along the road to be found, even when they have the | |
297 | 297 | same number. |
298 | : Example for given street name: | |
298 | : Example for given street name: | |
299 | 299 | :: Node - addr:street=Main Street addr:housenumber=2 |
300 | 300 | :: Way 1 - name=Main Street |
301 | 301 | :: Way 2 - name=Main Street, mkgmap:street=Main Street |
323 | 323 | |
324 | 324 | ;--overview-levels=level:resolution[,level:resolution...] |
325 | 325 | : Like levels, specifies additional levels that are to be written to the |
326 | overview map. Counting of the levels should continue. Up to 8 additional | |
327 | levels may be specified, but the lowest usable resolution with MapSource | |
326 | overview map. Counting of the levels should continue. Up to 8 additional | |
327 | levels may be specified, but the lowest usable resolution with MapSource | |
328 | 328 | seems to be 11. The hard coded default is empty. |
329 | : See also option --overview-dem-dist. | |
329 | : See also option --overview-dem-dist. | |
330 | 330 | |
331 | 331 | ;--remove-ovm-work-files |
332 | : If overview-levels is used, mkgmap creates one additional file | |
333 | with the prefix ovm_ for each map (*.img) file. | |
332 | : If overview-levels is used, mkgmap creates one additional file | |
333 | with the prefix ovm_ for each map (*.img) file. | |
334 | 334 | These files are used to create the overview map. |
335 | With option --remove-ovm-work-files=true the files are removed | |
336 | after the overview map was created. The default is to keep the files. | |
335 | With option --remove-ovm-work-files=true the files are removed | |
336 | after the overview map was created. The default is to keep the files. | |
337 | 337 | |
338 | 338 | === Style options === |
339 | 339 | |
356 | 356 | style file. |
357 | 357 | |
358 | 358 | ;--style=name |
359 | : Specify a style name. Must be used if --style-file points to a | |
360 | directory or zip file containing multiple styles. If --style-file | |
361 | is not used, it selects one of the built-in styles. | |
359 | : Specify a style name. Must be used if --style-file points to a | |
360 | directory or zip file containing multiple styles. If --style-file | |
361 | is not used, it selects one of the built-in styles. | |
362 | 362 | |
363 | 363 | ;--style-option=tag[=value][;tag[=value]...] |
364 | 364 | : Provide a semicolon separated list of tags which can be used in the style. |
365 | 365 | The intended use is to make a single style more flexible, e.g. |
366 | 366 | you may want to use a slightly different set of rules for a map of |
367 | 367 | a whole continent. The tags given will be prefixed with "mkgmap:option:". |
368 | If no value is provided the default "true" is used. | |
368 | If no value is provided the default "true" is used. | |
369 | 369 | This option allows to use rules like |
370 | 370 | mkgmap:option:light=true & landuse=farmland {remove landuse} |
371 | 371 | Example: -- style-option=light;routing=car |
372 | 372 | will add the tags mkgmap:option:light=true and mkgmap:option:routing=car |
373 | to each element before style processing happens. | |
373 | to each element before style processing happens. | |
374 | 374 | |
375 | 375 | ;--list-styles |
376 | 376 | : List the available styles. If this option is preceded by a style-file |
377 | 377 | option then it lists the styles available within that file. |
378 | 378 | |
379 | 379 | ;--check-styles |
380 | : Perform some checks on the available styles. If this option is | |
381 | preceded by a style-file option then it checks the styles | |
380 | : Perform some checks on the available styles. If this option is | |
381 | preceded by a style-file option then it checks the styles | |
382 | 382 | available within that file. If it is also preceded by the style |
383 | 383 | option it will only check that style. |
384 | 384 | |
448 | 448 | : Simplifies the ways with the Douglas Peucker algorithm. |
449 | 449 | NUM is the maximal allowed error distance, by which the resulting |
450 | 450 | way may differ from the original one. |
451 | This distance gets shifted with lower zoom levels. | |
451 | This distance gets shifted with lower zoom levels. | |
452 | 452 | Recommended setting is 4, this should lead to only small differences |
453 | 453 | (Default is 2.6, which should lead to invisible changes) |
454 | 454 | |
464 | 464 | |
465 | 465 | ;--min-size-polygon=NUM |
466 | 466 | : Removes all polygons smaller than NUM from the map. |
467 | This reduces map size and speeds up redrawing of maps. | |
467 | This reduces map size and speeds up redrawing of maps. | |
468 | 468 | Recommended value is 8 to 15, default is 8. |
469 | 469 | : See also polygon-size-limits. |
470 | 470 | |
471 | 471 | ;--polygon-size-limits=resolution:value[,resolution:value...] |
472 | 472 | : Allows you to specify different min-size-polygon values for each resolution. |
473 | Sample: | |
473 | Sample: | |
474 | 474 | :: --polygon-size-limits="24:12, 18:10, 16:8, 14:4, 12:2, 11:0" |
475 | : If a resolution is not given, mkgmap uses the value for the next higher | |
475 | : If a resolution is not given, mkgmap uses the value for the next higher | |
476 | 476 | one. For the given sample, resolutions 19 to 24 will use value 12, |
477 | 477 | resolution 17 and 18 will use 10, and so on. |
478 | Value 0 means to not apply the size filter. | |
478 | Value 0 means to not apply the size filter. | |
479 | 479 | Note that in resolution 24 the filter is not used. |
480 | 480 | The following options are equivalent: |
481 | 481 | :: --min-size-polygon=12 |
498 | 498 | formula sqrt(size/2) gives an integer value. |
499 | 499 | |
500 | 500 | ;--dem=path[,path...] |
501 | : The option expects a comma separated list of paths to directories or zip | |
502 | files containing *.hgt files. Directories are searched for *.hgt files | |
501 | : The option expects a comma separated list of paths to directories or zip | |
502 | files containing *.hgt files. Directories are searched for *.hgt files | |
503 | 503 | and also for *.hgt.zip and *.zip files. |
504 | : The list is searched in the given order, so if you want to use 1'' files | |
504 | : The list is searched in the given order, so if you want to use 1'' files | |
505 | 505 | make sure that they are found first. There are different sources for *.hgt |
506 | 506 | files, some have so called voids which are areas without data. Those should be |
507 | 507 | avoided. |
516 | 516 | distance between two points in the hgt file, that is 3314 for 1'' and 9942 for |
517 | 517 | 3''. Higher distances mean lower resolution and thus fewer bytes in the map. |
518 | 518 | Reasonable values for the highest resolution should not be much smaller than |
519 | 50% hgt resolution, that is somewhere between 1648 and 5520 for 1'' hgt input | |
519 | 50% hgt resolution, that is somewhere between 1648 and 5520 for 1'' hgt input | |
520 | 520 | files (3312 is often used), and 5520 to 9942 for 3'' hgt input files. |
521 | 521 | : Example which should work with levels="0:24, 1:22, 2:20, 3:18": |
522 | 522 | : --dem-dists=3312,13248,26512,53024 |
531 | 531 | resolution and dem-dist value, else bilinear is used. The default is auto. |
532 | 532 | |
533 | 533 | ;--dem-poly=filename |
534 | : If given, the filename should point to a *.poly file in osmosis polygon | |
534 | : If given, the filename should point to a *.poly file in osmosis polygon | |
535 | 535 | file format. The polygon described in the file is used to determine the area |
536 | 536 | for which DEM data should be added to the map. If not given, the DEM data will |
537 | 537 | cover the full tile area. |
541 | 541 | overview map. If not given or 0, mkgmap will not add DEM to the overview map. |
542 | 542 | Reasonable values depend on the size of the area and the lowest resolution |
543 | 543 | used for the single tiles, good compromises are somewhere between 55000 |
544 | and 276160. | |
544 | and 276160. | |
545 | 545 | |
546 | 546 | === Miscellaneous options === |
547 | 547 | |
569 | 569 | : Tells mkgmap to write NET data, which is needed for address search |
570 | 570 | and routing. Use this option if you want address search, but do |
571 | 571 | not need a map that supports routing or house number search. |
572 | <p> | |
572 | ||
573 | 573 | ;--route |
574 | : Tells mkgmap to write NET and NOD data, which are needed in maps | |
574 | : Tells mkgmap to write NET and NOD data, which are needed in maps | |
575 | 575 | that support routing. If you specify this option, you do not need |
576 | 576 | to specify --net and --no-net is ignored. |
577 | 577 | |
595 | 595 | |
596 | 596 | ;--drive-on=left|right|detect|detect,left|detect,right |
597 | 597 | : Explicitly specify which side of the road vehicles are |
598 | expected to drive on. | |
599 | If the first option is detect, the program tries | |
598 | expected to drive on. | |
599 | If the first option is detect, the program tries | |
600 | 600 | to find out the proper flag. If that detection |
601 | 601 | fails, the second value is used (or right if none is given). |
602 | With OSM data as input, the detection tries to find out | |
602 | With OSM data as input, the detection tries to find out | |
603 | 603 | the country each road is in and compares the number |
604 | 604 | of drive-on-left roads with the rest. |
605 | Use the --bounds option to make sure that the detection | |
606 | finds the correct country. | |
605 | Use the --bounds option to make sure that the detection | |
606 | finds the correct country. | |
607 | 607 | |
608 | 608 | ;--check-roundabouts |
609 | 609 | : Check that roundabouts have the expected direction (clockwise |
660 | 660 | ;--cycle-map |
661 | 661 | : Tells mkgmap that the map is for cyclists. This assumes that |
662 | 662 | different vehicles are different kinds of bicycles, e.g. a way |
663 | with mkgmap:car=yes and mkgmap:bicycle=no may be a road that is | |
663 | with mkgmap:car=yes and mkgmap:bicycle=no may be a road that is | |
664 | 664 | good for racing bikes, but not for other cyclists. |
665 | 665 | This allows the optimisation of sharp angles at junctions of those roads. |
666 | 666 | Don't use with the default style as that is a general style! |
674 | 674 | ;--report-dead-ends=LEVEL |
675 | 675 | : Set the dead end road warning level. The value of LEVEL (which |
676 | 676 | defaults to 1 if this option is not specified) determines |
677 | those roads to report: 0 = none, 1 = multiple one-way roads | |
678 | that join together but go nowhere, 2 = individual one-way roads | |
679 | that go nowhere. | |
677 | those roads to report: | |
678 | :* 0 = none | |
679 | :* 1 = report on connected one-way roads that go nowhere | |
680 | :* 2 = also report on individual one-way roads that go nowhere. | |
681 | ||
682 | ;--dead-ends[=key[=value]][,key[=value]...] | |
683 | : Specify a list of keys and optional values that should be considered | |
684 | to be valid dead ends when found on the node at the end of a way. Ways with | |
685 | nodes matching any of the items in the list will not be reported as dead ends. | |
686 | If no value or * is specified for value then presence of the key alone will | |
687 | cause the dead end check to be skipped. The default is --dead-ends=fixme,FIXME. | |
680 | 688 | |
681 | 689 | ;--add-pois-to-lines |
682 | 690 | : Generate POIs for lines. For each line (must not be closed) POIs are |
686 | 694 | the following values: |
687 | 695 | :* start - The first point of the line |
688 | 696 | :* end - The last point of the line |
689 | :* inner - Each point of the line except the first and the last | |
697 | :* inner - Each point of the line except the first and the last | |
690 | 698 | :* mid - The middle point |
699 | ||
691 | 700 | ;--add-pois-to-areas |
692 | : Generate a POI for each polygon and multipolygon. The POIs are created | |
693 | after the relation style but before the other styles are applied. Each | |
701 | : Generate a POI for each polygon and multipolygon. The POIs are created | |
702 | after the relation style but before the other styles are applied. Each | |
694 | 703 | POI is tagged with the same tags of |
695 | 704 | the area/multipolygon. Additionally the tag mkgmap:area2poi=true is |
696 | 705 | set so that it is possible to use that information in the points style |
697 | 706 | file. Artificial polygons created by multipolyon processing are not used. |
698 | The POIs are created at the following positions (first rule that applies): | |
699 | :;polygons: | |
700 | ::First rule that applies of | |
707 | The POIs are created at the following positions: | |
708 | :;polygons: the first rule that applies of: | |
701 | 709 | ::* the first node tagged with a tag defined by the --pois-to-areas-placement option |
702 | ::* the centre point | |
703 | :;multipolygons: | |
704 | ::First rule that applies of | |
710 | ::* the centre point | |
711 | :;multipolygons: the first rule that applies of: | |
705 | 712 | ::* the node with role=label |
706 | 713 | ::* the centre point of the biggest area |
714 | ||
707 | 715 | ;--pois-to-areas-placement=tag=value[;tag=value...] |
708 | 716 | : A POI is placed at the first node of the polygon tagged with the first tag/value |
709 | 717 | pair. If none of the nodes are tagged with the first tag-value pair the first node |
714 | 722 | Default: entrance=main;entrance=yes;building=entrance |
715 | 723 | |
716 | 724 | ;--precomp-sea=directory|zipfile |
717 | : Defines the directory or a zip file that contains precompiled sea tiles. | |
725 | : Defines the directory or a zip file that contains precompiled sea tiles. | |
718 | 726 | Sea files in a zip file must be located in the zip file's root directory or in |
719 | 727 | a sub directory sea. When this option is defined all natural=coastline tags |
720 | 728 | from the input OSM tiles are removed and the precompiled data is used instead. |
722 | 730 | and land-tag. The coastlinefile option is ignored if precomp-sea is set. |
723 | 731 | |
724 | 732 | ;--coastlinefile=filename[,filename...] |
725 | : Defines a comma separated list of files that contain coastline | |
726 | data. The coastline data from the input files is removed if | |
733 | : Defines a comma separated list of files that contain coastline | |
734 | data. The coastline data from the input files is removed if | |
727 | 735 | this option is set. Files must have OSM or PBF file format. |
728 | 736 | |
729 | 737 | ;--generate-sea[=ValueList] |
753 | 761 | :;close-gaps=NUM |
754 | 762 | :: close gaps in coastline that are less than this distance (metres) |
755 | 763 | |
756 | :;floodblocker | |
764 | :;floodblocker | |
757 | 765 | :: enable the flood blocker that prevents a flooding of |
758 | 766 | land by checking if the sea polygons contain streets |
759 | 767 | (works only with multipolygon processing) |
760 | 768 | |
761 | :;fbgap=NUM | |
769 | :;fbgap=NUM | |
762 | 770 | :: flood blocker gap in metre (default 40) |
763 | 771 | points that are closer to the sea polygon do not block |
764 | 772 | |
765 | 773 | :;fbthres=NUM |
766 | :: at least so many highway points must be contained in | |
774 | :: at least so many highway points must be contained in | |
767 | 775 | a sea polygon so that it may be removed by the flood |
768 | 776 | blocker (default 20) |
769 | 777 | |
770 | 778 | :;fbratio=NUM |
771 | :: only sea polygons with a higher ratio | |
772 | (highway points * 100000 / polygon size) are removed | |
779 | :: only sea polygons with a higher ratio | |
780 | (highway points * 100000 / polygon size) are removed | |
773 | 781 | (default 0.5) |
774 | 782 | |
775 | 783 | :;fbdebug |
791 | 799 | the original that allows bicycle traffic (in both directions). |
792 | 800 | |
793 | 801 | ;--link-pois-to-ways |
794 | : This option may copy some specific attributes of a POI | |
802 | : This option may copy some specific attributes of a POI | |
795 | 803 | to a small part of the way the POI is located on. This can be used |
796 | 804 | to let barriers block a way or to lower the calculated speed |
797 | 805 | around traffic signals. |
798 | POIs with the tags highway=* (e.g. highway=traffic_signals) | |
806 | POIs with the tags highway=* (e.g. highway=traffic_signals) | |
799 | 807 | or barrier=* (e.g. barrier=cycle_barrier) are supported. |
800 | 808 | The style developer must add at least one of the access tags |
801 | (mkgmap:foot, mkgmap:car etc.), mkgmap:road-speed and/or | |
802 | mkgmap:road-class to the POI. | |
803 | The access tags are ignored if they have no effect for the way, | |
804 | else a route restriction is added at the POI so that only | |
805 | allowed vehicles are routed through it. | |
806 | The tags mkgmap:road-speed and/or mkgmap:road-class are | |
809 | (mkgmap:foot, mkgmap:car etc.), mkgmap:road-speed and/or | |
810 | mkgmap:road-class to the POI. | |
811 | The access tags are ignored if they have no effect for the way, | |
812 | else a route restriction is added at the POI so that only | |
813 | allowed vehicles are routed through it. | |
814 | The tags mkgmap:road-speed and/or mkgmap:road-class are | |
807 | 815 | applied to a small part of the way around the POI, typically |
808 | 816 | to the next junction or a length of ~25m. The tags |
809 | are ignored for pedestrian-only ways. | |
817 | are ignored for pedestrian-only ways. | |
810 | 818 | |
811 | 819 | ;--process-destination |
812 | 820 | : Splits all motorway_link, trunk_link, primary_link, secondary_link, |
813 | 821 | and tertiary_link ways tagged with destination into two or three parts where |
814 | 822 | the second part is additionally tagged with mkgmap:dest_hint=*. |
815 | The code checks for the tags destination, destination:lanes, | |
823 | The code checks for the tags destination, destination:lanes, | |
816 | 824 | destination:street and some variants with :forward/:backward like |
817 | 825 | destination:forward or destination:lanes:backward. If a value for |
818 | destination is found, the special tag mkgmap:dest_hint is set to | |
826 | destination is found, the special tag mkgmap:dest_hint is set to | |
819 | 827 | it and the way is split. |
820 | 828 | This happens before the style rules are processed. |
821 | 829 | This allows to use any routable Garmin type (except 0x08 and 0x09) |
824 | 832 | : See also --process-exits. |
825 | 833 | |
826 | 834 | ;--process-exits |
827 | : Usual Garmin devices do not tell the name of the exit on motorways | |
835 | : Usual Garmin devices do not tell the name of the exit on motorways | |
828 | 836 | while routing with mkgmap created maps. This option splits each |
829 | motorway_link, trunk_link, primary_link, secondary_link, and | |
830 | tertiary_link way into three parts. | |
831 | All parts are tagged with the original tags of the link. | |
837 | motorway_link, trunk_link, primary_link, secondary_link, and | |
838 | tertiary_link way into three parts. | |
839 | All parts are tagged with the original tags of the link. | |
832 | 840 | Additionally the middle part is tagged with the following tags: |
833 | 841 | |
834 | 842 | :: mkgmap:exit_hint=true |
837 | 845 | :: mkgmap:exit_hint_exit_to=<exit_to tag value of the exit> |
838 | 846 | |
839 | 847 | : Adding a rule checking the mkgmap:exit_hint=true makes it possible |
840 | to use any routable Garmin type (except 0x08 and 0x09) for the middle | |
841 | part so that the Garmin device tells the name of this middle part as | |
848 | to use any routable Garmin type (except 0x08 and 0x09) for the middle | |
849 | part so that the Garmin device tells the name of this middle part as | |
842 | 850 | hint where to leave the motorway/trunk. |
843 | The first part must have type 0x08 or 0x09 so that Garmin uses the hint. | |
851 | The first part must have type 0x08 or 0x09 so that Garmin uses the hint. | |
844 | 852 | |
845 | 853 | ;--delete-tags-file=filename |
846 | 854 | : Names a file that should contain one or more lines of the form |
856 | 864 | an overview map. The options --nsis and --gmapi imply --tdbfile. |
857 | 865 | |
858 | 866 | ;--show-profiles=1 |
859 | : Sets a flag in tdb file. The meaning depends on the availability of DEM | |
860 | data (see "Hill Shading (DEM) options"). | |
861 | : Without DEM data the flag enables profile calculation in MapSource or | |
862 | Basecamp based on information from contour lines. | |
863 | : If DEM data is available the profile is calculated with that | |
867 | : Sets a flag in tdb file. The meaning depends on the availability of DEM | |
868 | data (see "Hill Shading (DEM) options"). | |
869 | : Without DEM data the flag enables profile calculation in MapSource or | |
870 | Basecamp based on information from contour lines. | |
871 | : If DEM data is available the profile is calculated with that | |
864 | 872 | information and the flag only changes the status line to show the height when |
865 | you hover over an area with valid DEM data. | |
873 | you hover over an area with valid DEM data. | |
866 | 874 | : The default is show-profiles=0. |
867 | 875 | |
868 | 876 | ;--transparent |
884 | 892 | |
885 | 893 | ;--hide-gmapsupp-on-pc |
886 | 894 | : Set a bit in the gmapsupp.img that tells PC software that the file is |
887 | already installed on the PC and therefore there is no need to read it | |
895 | already installed on the PC and therefore there is no need to read it | |
888 | 896 | from the device. |
889 | 897 | |
890 | 898 | ;--poi-address |
902 | 910 | that smaller features are rendered over larger ones |
903 | 911 | (assuming the draw order is equal). |
904 | 912 | The tag mkgmap:drawLevel can be used to override the |
905 | natural area of a polygon, so forcing changes to the rendering order. | |
913 | natural area of a polygon, so forcing changes to the rendering order. | |
906 | 914 | |
907 | 915 | === Deprecated and Obsolete Options === |
908 | 916 | |
909 | 917 | ;--drive-on-left |
910 | 918 | ;--drive-on-right |
911 | 919 | : Deprecated; use drive-on instead. |
912 | The options are translated to drive-on=left|right. | |
920 | The options are translated to drive-on=left|right. | |
913 | 921 | |
914 | 922 | ;--make-all-cycleways |
915 | 923 | : Deprecated; use --make-opposite-cycleways instead. Former meaning: |
966 | 974 | |
967 | 975 | : Optional BITMASK (default value 3) allows you to specify which |
968 | 976 | adjustments are to be made (where necessary): |
969 | ||
970 | 977 | :: 1 = increase angle between side road and outgoing main road |
971 | 978 | :: 2 = increase angle between side road and incoming main road |
13 | 13 | People who have contributed suggestions and corrections to this document |
14 | 14 | are: |
15 | 15 | Carlos Dávila, |
16 | Geoff Sherlock | |
17 | ||
16 | Geoff Sherlock, | |
17 | Ticker Berkin | |
18 | 18 | |
19 | 19 | The list of nicknames of everyone that had modified the wiki pages at the time that |
20 | 20 | this manual was created is as follows: |
0 | ||
1 | = Conversion Style manual | |
2 | The mkgmap team | |
3 | v1.0, December 2012 | |
4 | :toc: | |
5 | :numbered: | |
6 | :website: http://www.mkgmap.org.uk | |
7 | :email: mkgmap-dev@lists.mkgmap.org.uk | |
8 | :description: Describes the style language that converts from OSM tags to Garmin types. | |
9 | ||
10 | Introduction | |
11 | ------------ | |
12 | ||
13 | This manual explains how to write a mkgmap style to convert | |
14 | between OSM tags and features on a Garmin GPS device. | |
15 | ||
16 | A style is used to choose which OSM map features appear in the | |
17 | Garmin map and which Garmin symbols are used. | |
18 | ||
19 | There are a few styles built into mkgmap, but | |
20 | as there are many different purposes a map may used for, the default | |
21 | styles in mkgmap will not be ideal for everyone, so | |
22 | you can create and use styles external to mkgmap. | |
23 | ||
24 | The term _style_ could mean the actual way that the features appear on | |
25 | a GPS device, the colour, thickness of the line and so on. This manual | |
26 | does not cover that, and if that is what you are looking for, then you | |
27 | need the documentation for *TYP files*. | |
28 | ||
29 | Few people will want to write their own style from scratch, most people | |
30 | will use the built in conversion style, or at most make a few changes | |
31 | to the default style to add or remove a small number of features. | |
32 | For general information about running and using mkgmap see the | |
33 | *Tutorial document*. | |
34 | ||
35 | To be clear this is only needed for converting OSM tags, if you are | |
36 | starting with a Polish format file, there is no style involved as the | |
37 | garmin types are already fully specified in the input file. | |
38 | ||
39 | For general information about the Open Street Map project see the | |
40 | http://wiki.openstreetmap.org[Open Street Map wiki]. | |
41 | ||
42 | ||
43 | :leveloffset: 1 | |
44 | ||
45 | include::design.txt[] | |
46 | ||
47 | include::files.txt[] | |
48 | ||
49 | include::rules.txt[] | |
50 | ||
51 | include::creating.txt[] |
243 | 243 | Functions calculate a specific property of an OSM element. |
244 | 244 | |
245 | 245 | .Style functions |
246 | [width="100%",cols="2,1,1,1,5",options="header"] | |
246 | [width="100%",cols="5,1,1,1,11",options="header"] | |
247 | 247 | |===== |
248 | |Function |Node |Way |Relation |Description | |
248 | |Function |Node |Way |Rel. |Description | |
249 | 249 | |length() | | x | x | |
250 | 250 | Calculates the length in m. For relations its the sum of all member length (including sub relations). |
251 | 251 | |
280 | 280 | |osmid() | x | x | x | |
281 | 281 | Retrieves the id of the OSM element. This can be useful for style debugging purposes. Note that due to internal changes like merging, cutting etc. |
282 | 282 | some element ids are changed and some have a faked id > 4611686018427387904. |
283 | ||
284 | |is_in(*tag*,*value*,*method*) | x | x | | | |
285 | +true+ if the element is in polygon(s) having the specified *tag*=*value* according to the *method*, +false+ otherwise. | |
286 | The methods available depend on the Style section: | |
287 | ||
288 | polygons: | |
289 | *all* - all of the closed 'way' is within the polygon(s). | |
290 | *any* - some is within. | |
291 | ||
292 | points: | |
293 | *in* - the 'node' is within a polygon. | |
294 | *in_or_on* - it is within or on the edge. | |
295 | *on* - it is on the edge. | |
296 | ||
297 | lines: | |
298 | *all* - part of the 'way' is within the polygon(s), none is outside; it might touch an edge. | |
299 | *all_in_or_on* - none is outside. | |
300 | *on* - it runs along the edge. | |
301 | *any* - part is within. | |
302 | *none* - part is outside, none is inside. | |
303 | ||
304 | A common case is a line outside the polygon that runs to the edge, joining a line that is inside. | |
305 | The method to match an outside line (*none*) allows part to be on the edge, | |
306 | likewise, the method to match an inside line (*all*) allows part to be on the edge. | |
307 | Compared to *all*, the method *all_in_or_on* additionally matches lines which are only on the edge of the polygon. | |
283 | 308 | |
284 | 309 | |==== |
285 | 310 |
0 | 0 | = Conversion Style manual |
1 | 1 | The mkgmap team |
2 | :pubdate: January 2013 | |
2 | :pubdate: set to date of pdf generation | |
3 | 3 | :toc: |
4 | 4 | :numbered: |
5 | 5 | :doctype: book |
7 | 7 | :email: mkgmap-dev@lists.mkgmap.org.uk |
8 | 8 | :description: Describes the style language that converts from OSM tags to Garmin types. |
9 | 9 | :max-width: 58em |
10 | ||
11 | //// | |
12 | To generate style-manual.pdf you will need asciidoc, fop, python-pygments, mkgmap-pygments. | |
13 | eg. for a Fedora system, as superuser: | |
14 | # dnf install asciidoc.noarch | |
15 | # dnf install fop.noarch | |
16 | # dnf install python-pygments.noarch | |
17 | # pip install mkgmap-pygments | |
18 | ||
19 | Then, as normal user, cd to this directory (doc/styles) and | |
20 | $ make pdf | |
21 | //// | |
10 | 22 | |
11 | 23 | :frame: topbot |
12 | 24 | :grid: rows |
20 | 20 | specify -Xmx4g - note there is no space or equals sign in the option. |
21 | 21 | |
22 | 22 | -enableassertions |
23 | -ea | |
23 | 24 | Causes an error to be thrown if an assertion written in the mkgmap code is |
24 | 25 | evaluated as not true. This is useful in detecting bugs in the mkgmap code. |
25 | 26 | |
183 | 184 | beginning and end of the important part. In combination with option |
184 | 185 | --split-name-index only the words in the important part are indexed. |
185 | 186 | |
186 | ||
187 | 187 | There are two main effects of this option: |
188 | 188 | - On the PC, when zooming out, the name 'Rue de la Concorde' is only |
189 | 189 | rendered as 'Concorde'. |
193 | 193 | Concorde' or 'Rue du Moulin'. It may list 'Rueben Brookins Road' if |
194 | 194 | that is in the map. Only MapSource shows a corresponding hint. |
195 | 195 | |
196 | ||
197 | 196 | Another effect is that the index is smaller. |
198 | 197 | See comments in the sample roadNameConfig.txt for further details. |
199 | 198 | |
227 | 226 | like 'Planet' when you search for 'Net', it doesn't use the index because |
228 | 227 | the index created by mkgmap cannot help for that search. |
229 | 228 | |
230 | ||
231 | 229 | So, this option may help when you care about the size of the index or the |
232 | 230 | memory that is needed to calculate it. The option expects a comma separated |
233 | 231 | list of types or type ranges. A range is given with from-type-to-type, e.g. |
251 | 249 | mkgmap:region etc. using these values. |
252 | 250 | |
253 | 251 | The following special tags are added: |
254 | mkgmap:admin_level2 : Name of the admin_level=2 boundary | |
252 | mkgmap:admin_level2 : Name of the admin_level=2 boundary | |
255 | 253 | mkgmap:admin_level3 : Name of the admin_level=3 boundary |
256 | 254 | .. |
257 | 255 | mkgmap:admin_level11 |
274 | 272 | automatic assignment of address fields is somehow a best guess. |
275 | 273 | is_in |
276 | 274 | The is_in tag is analysed for country and region information. |
277 | ||
278 | ||
279 | 275 | nearest |
280 | 276 | The city/hamlet points that are closest to the element are used to |
281 | 277 | assign the missing address fields. Beware that cities located in the |
559 | 555 | routing. Use this option if you want address search, but do not need a map |
560 | 556 | that supports routing or house number search. |
561 | 557 | |
562 | ||
563 | 558 | --route |
564 | 559 | Tells mkgmap to write NET and NOD data, which are needed in maps that |
565 | 560 | support routing. If you specify this option, you do not need to specify |
657 | 652 | |
658 | 653 | --report-dead-ends=LEVEL |
659 | 654 | Set the dead end road warning level. The value of LEVEL (which defaults to |
660 | 1 if this option is not specified) determines those roads to report: 0 = | |
661 | none, 1 = multiple one-way roads that join together but go nowhere, 2 = | |
662 | individual one-way roads that go nowhere. | |
655 | 1 if this option is not specified) determines those roads to report: | |
656 | * 0 = none | |
657 | * 1 = report on connected one-way roads that go nowhere | |
658 | * 2 = also report on individual one-way roads that go nowhere. | |
659 | ||
660 | --dead-ends[=key[=value]][,key[=value]...] | |
661 | Specify a list of keys and optional values that should be considered to be | |
662 | valid dead ends when found on the node at the end of a way. Ways with nodes | |
663 | matching any of the items in the list will not be reported as dead ends. If | |
664 | no value or * is specified for value then presence of the key alone will | |
665 | cause the dead end check to be skipped. The default is | |
666 | --dead-ends=fixme,FIXME. | |
663 | 667 | |
664 | 668 | --add-pois-to-lines |
665 | 669 | Generate POIs for lines. For each line (must not be closed) POIs are |
671 | 675 | * end - The last point of the line |
672 | 676 | * inner - Each point of the line except the first and the last |
673 | 677 | * mid - The middle point |
678 | ||
674 | 679 | --add-pois-to-areas |
675 | 680 | Generate a POI for each polygon and multipolygon. The POIs are created |
676 | 681 | after the relation style but before the other styles are applied. Each POI |
677 | 682 | is tagged with the same tags of the area/multipolygon. Additionally the tag |
678 | 683 | mkgmap:area2poi=true is set so that it is possible to use that information |
679 | 684 | in the points style file. Artificial polygons created by multipolyon |
680 | processing are not used. The POIs are created at the following positions | |
681 | (first rule that applies): | |
682 | polygons: | |
683 | First rule that applies of | |
685 | processing are not used. The POIs are created at the following positions: | |
686 | polygons: the first rule that applies of: | |
684 | 687 | * the first node tagged with a tag defined by the |
685 | 688 | --pois-to-areas-placement option |
686 | 689 | * the centre point |
687 | multipolygons: | |
688 | First rule that applies of | |
690 | multipolygons: the first rule that applies of: | |
689 | 691 | * the node with role=label |
690 | 692 | * the centre point of the biggest area |
693 | ||
691 | 694 | --pois-to-areas-placement=tag=value[;tag=value...] |
692 | 695 | A POI is placed at the first node of the polygon tagged with the first |
693 | 696 | tag/value pair. If none of the nodes are tagged with the first tag-value |
936 | 939 | |
937 | 940 | Optional BITMASK (default value 3) allows you to specify which adjustments |
938 | 941 | are to be made (where necessary): |
939 | ||
940 | 942 | 1 = increase angle between side road and outgoing main road |
941 | 943 | 2 = increase angle between side road and incoming main road |
0 | svn.version: 4454 | |
1 | build.timestamp: 2020-02-20T09:16:25+0000 | |
0 | svn.version: 4475 | |
1 | build.timestamp: 2020-03-26T06:28:05+0000 |
767 | 767 | FontStyle=NoLabel (invisible) |
768 | 768 | CustomColor=No |
769 | 769 | Xpm="32 32 2 1" |
770 | ". c none" | |
771 | "1 c #FFFFFF" | |
772 | "................................" | |
773 | "................................" | |
774 | "................................" | |
775 | "................................" | |
776 | "................................" | |
777 | "................................" | |
778 | "................................" | |
779 | "................................" | |
780 | "................................" | |
781 | "................................" | |
782 | "................................" | |
783 | "................................" | |
784 | "................................" | |
785 | "................................" | |
786 | "................................" | |
787 | "................................" | |
788 | "................................" | |
789 | "................................" | |
790 | "................................" | |
791 | "................................" | |
792 | "................................" | |
793 | "................................" | |
794 | "................................" | |
795 | "................................" | |
796 | "................................" | |
797 | "................................" | |
798 | "................................" | |
799 | "................................" | |
800 | "................................" | |
801 | "................................" | |
802 | "................................" | |
803 | "................................" | |
804 | ; "12345678901234567890123456789012" | |
770 | ". c none" | |
771 | "1 c #FFFFFF" | |
772 | "................................" | |
773 | "................................" | |
774 | "................................" | |
775 | "................................" | |
776 | "................................" | |
777 | "................................" | |
778 | "................................" | |
779 | "................................" | |
780 | "................................" | |
781 | "................................" | |
782 | "................................" | |
783 | "................................" | |
784 | "................................" | |
785 | "................................" | |
786 | "................................" | |
787 | "................................" | |
788 | "................................" | |
789 | "................................" | |
790 | "................................" | |
791 | "................................" | |
792 | "................................" | |
793 | "................................" | |
794 | "................................" | |
795 | "................................" | |
796 | "................................" | |
797 | "................................" | |
798 | "................................" | |
799 | "................................" | |
800 | "................................" | |
801 | "................................" | |
802 | "................................" | |
803 | "................................" | |
804 | ;12345678901234567890123456789012 | |
805 | 805 | [end] |
806 | 806 | [_polygon] |
807 | 807 | type=0x3f |
1955 | 1955 | " !! !! !! !! !! !! !! !! " |
1956 | 1956 | " !! !! !! !! !! !! !! !! " |
1957 | 1957 | ;12345678901234567890123456789012 |
1958 | String=River, Wadi (Intermittent) | |
1958 | String=River/Wadi (Intermittent) | |
1959 | 1959 | String1=0x01,Cours d’eau (intermittent) |
1960 | 1960 | String2=0x02,Fluß (Periodisch) |
1961 | 1961 | String4=0x03,Rivier (Periodiek) |
5757 | 5757 | type=0x065 |
5758 | 5758 | subtype=0x0f |
5759 | 5759 | ;GRMN_TYPE: Geographical Named Points of Interest - Water Related/RESERVOIR/Reservoir/Non NT, NT |
5760 | String=Reserviour | |
5760 | String=Reservoir | |
5761 | 5761 | String1=0x01,Eau |
5762 | 5762 | String2=0x02,Wasser |
5763 | 5763 | String4=0x03,Water |
40 | 40 | private static final short PRESERVED_MASK = 0x0002; // bit in flags is true if point should not be filtered out |
41 | 41 | private static final short REPLACED_MASK = 0x0004; // bit in flags is true if point was replaced |
42 | 42 | private static final short ADDED_BY_CLIPPER_MASK = 0x0008; // bit in flags is true if point was added by clipper |
43 | private static final short FIXME_NODE_MASK = 0x0010; // bit in flags is true if a node with this coords has a fixme tag | |
43 | private static final short SKIP_DEAD_END_CHECK_NODE_MASK = 0x0010; // bit in flags is true if a node with this coords has a tag listed in --dead-ends | |
44 | 44 | private static final short REMOVE_MASK = 0x0020; // bit in flags is true if this point should be removed |
45 | 45 | private static final short VIA_NODE_MASK = 0x0040; // bit in flags is true if a node with this coords is the via node of a RestrictionRelation |
46 | 46 | |
55 | 55 | public static final int DELTA_SHIFT = HIGH_PREC_BITS - 24; |
56 | 56 | private static final int MAX_DELTA = 1 << (DELTA_SHIFT - 2); // max delta abs value that is considered okay |
57 | 57 | private static final long FACTOR_HP = 1L << HIGH_PREC_BITS; |
58 | private static final int HIGH_PREC_UNUSED_BITS = Integer.SIZE - HIGH_PREC_BITS; | |
58 | 59 | |
59 | 60 | public static final double R = 6378137.0; // Radius of earth at equator as defined by WGS84 |
60 | 61 | public static final double U = R * 2 * Math.PI; // circumference of earth at equator (WGS84) |
224 | 225 | } |
225 | 226 | |
226 | 227 | /** |
227 | * Mark the Coord to be treated like a Node in short arc removal | |
228 | * @param treatAsNode true or false | |
228 | * Mark the Coord as added by clipper | |
229 | * @param b true or false | |
229 | 230 | */ |
230 | 231 | public void setAddedByClipper(boolean b) { |
231 | 232 | if (b) |
232 | 233 | this.flags |= ADDED_BY_CLIPPER_MASK; |
233 | 234 | else |
234 | this.flags &= ~ADDED_BY_CLIPPER_MASK; | |
235 | this.flags &= ~ADDED_BY_CLIPPER_MASK; | |
235 | 236 | } |
236 | 237 | |
237 | 238 | /** |
238 | * Does this coordinate belong to a node with a fixme tag? | |
239 | * Does this coordinate belong to a node with a tag specified in --dead-ends? | |
239 | 240 | * Note that the value is set after evaluating the points style. |
240 | * @return true if the fixme flag is set, else false | |
241 | */ | |
242 | public boolean isFixme() { | |
243 | return (flags & FIXME_NODE_MASK) != 0; | |
244 | } | |
245 | ||
246 | public void setFixme(boolean b) { | |
241 | * @return true if the flag is set, else false | |
242 | */ | |
243 | public boolean isSkipDeadEndCheck() { | |
244 | return (flags & SKIP_DEAD_END_CHECK_NODE_MASK) != 0; | |
245 | } | |
246 | ||
247 | public void setSkipDeadEndCheck(boolean b) { | |
247 | 248 | if (b) |
248 | this.flags |= FIXME_NODE_MASK; | |
249 | else | |
250 | this.flags &= ~FIXME_NODE_MASK; | |
249 | this.flags |= SKIP_DEAD_END_CHECK_NODE_MASK; | |
250 | else | |
251 | this.flags &= ~SKIP_DEAD_END_CHECK_NODE_MASK; | |
251 | 252 | } |
252 | 253 | |
253 | 254 | public boolean isToRemove() { |
258 | 259 | if (b) |
259 | 260 | this.flags |= REMOVE_MASK; |
260 | 261 | else |
261 | this.flags &= ~REMOVE_MASK; | |
262 | this.flags &= ~REMOVE_MASK; | |
262 | 263 | } |
263 | 264 | |
264 | 265 | /** |
515 | 516 | } |
516 | 517 | |
517 | 518 | /** |
519 | * Distance to other point in high precision squared units | |
520 | */ | |
521 | public long distanceInHighPrecSquared(Coord other) { | |
522 | int dLatHp = other.getHighPrecLat() - getHighPrecLat(); | |
523 | int dLonHp = other.getHighPrecLon() - getHighPrecLon(); | |
524 | dLonHp = (dLonHp << HIGH_PREC_UNUSED_BITS) >> HIGH_PREC_UNUSED_BITS; // fix wrap-around earth | |
525 | return (long)dLatHp * dLatHp + (long)dLonHp * dLonHp; | |
526 | } | |
527 | ||
528 | /** | |
518 | 529 | * Calculate point on the line this->other. If d is the distance between this and other, |
519 | 530 | * the point is {@code fraction * d} metres from this. |
520 | 531 | * For small distances between this and other we use a flat earth approximation, |
524 | 535 | public Coord makeBetweenPoint(Coord other, double fraction) { |
525 | 536 | int dlatHp = other.getHighPrecLat() - getHighPrecLat(); |
526 | 537 | int dlonHp = other.getHighPrecLon() - getHighPrecLon(); |
527 | if (dlonHp == 0 || Math.abs(dlatHp) < 1000000 && Math.abs(dlonHp) < 1000000 ){ | |
538 | if (dlonHp == 0 || Math.abs(dlatHp) < 1000000 && Math.abs(dlonHp) < 1000000) { | |
528 | 539 | // distances are rather small, we can use flat earth approximation |
529 | int latHighPrec = (int) (getHighPrecLat() + dlatHp * fraction); | |
530 | int lonHighPrec = (int) (getHighPrecLon() + dlonHp * fraction); | |
540 | int latHighPrec = (int)Math.round(getHighPrecLat() + dlatHp * fraction); | |
541 | int lonHighPrec = (int)Math.round(getHighPrecLon() + dlonHp * fraction); | |
531 | 542 | return makeHighPrecCoord(latHighPrec, lonHighPrec); |
532 | 543 | } |
533 | 544 | double brng = this.bearingToOnRhumbLine(other, true); |
534 | 545 | double dist = this.distance(other) * fraction; |
535 | return this.destOnRhumLine(dist, brng); | |
546 | return this.destOnRhumbLine(dist, brng); | |
536 | 547 | } |
537 | 548 | |
538 | 549 | |
762 | 773 | * @param brng bearing in degrees |
763 | 774 | * @return a new Coord instance |
764 | 775 | */ |
765 | public Coord destOnRhumLine(double dist, double brng){ | |
776 | public Coord destOnRhumbLine(double dist, double brng){ | |
766 | 777 | double distRad = dist / R; // angular distance in radians |
767 | 778 | double lat1 = hpToRadians(this.getHighPrecLat()); |
768 | 779 | double lon1 = hpToRadians(this.getHighPrecLon()); |
812 | 823 | // simple calculation using Herons formula will fail |
813 | 824 | // calculate x, the point on line a-b which is as far away from a as this point |
814 | 825 | double b_ab = a.bearingToOnRhumbLine(b, true); |
815 | Coord x = a.destOnRhumLine(ap, b_ab); | |
826 | Coord x = a.destOnRhumbLine(ap, b_ab); | |
816 | 827 | // this dist between these two points is not exactly |
817 | 828 | // the perpendicul distance, but close enough |
818 | 829 | dist = x.distance(this); |
879 | 890 | double newLon = lon + Math.atan2(Math.sin(bearing) * Math.sin(angularDistance) * Math.cos(lat), Math.cos(angularDistance) - Math.sin(lat) * Math.sin(newLat)); |
880 | 891 | return new Coord(Math.toDegrees(newLat), Math.toDegrees(newLon)); |
881 | 892 | } |
882 | ||
893 | ||
894 | /** | |
895 | * Calculate if this point lies on the left or right of the line through the given points. | |
896 | * @param p0 first point | |
897 | * @param p2 second point | |
898 | * @return positive value if on left, negative value if on the right, 0 if on the line | |
899 | */ | |
900 | public long isLeft(final Coord p1, final Coord p2) { | |
901 | long p1Lat = p1.getHighPrecLat(); | |
902 | long p1Lon = p1.getHighPrecLon(); | |
903 | return ((long) p2.getHighPrecLon() - p1Lon) * ((long) this.getHighPrecLat() - p1Lat) | |
904 | - ((long) p2.getHighPrecLat() - p1Lat) * ((long) this.getHighPrecLon() - p1Lon); | |
905 | } | |
883 | 906 | } |
136 | 136 | FileChannel chan = FileChannel.open(Paths.get(name), StandardOpenOption.READ); |
137 | 137 | return openFs(name, chan); |
138 | 138 | } catch (IOException e) { |
139 | throw new FileNotFoundException("Failed to create or open file"); | |
139 | throw new FileNotFoundException("Failed to create or open file " + name); | |
140 | 140 | } |
141 | 141 | } |
142 | 142 |
186 | 186 | public boolean containsExpression(String exp) { |
187 | 187 | return expression.toString().contains(exp); |
188 | 188 | } |
189 | ||
190 | @Override | |
191 | public void augmentWith(uk.me.parabola.mkgmap.reader.osm.ElementSaver elementSaver) { | |
192 | expression.augmentWith(elementSaver); | |
193 | } | |
194 | ||
189 | 195 | } |
21 | 21 | import java.io.FileNotFoundException; |
22 | 22 | import java.io.InputStreamReader; |
23 | 23 | import java.io.Reader; |
24 | import java.io.UnsupportedEncodingException; | |
25 | 24 | import java.nio.charset.StandardCharsets; |
26 | 25 | import java.util.ArrayList; |
27 | 26 | import java.util.List; |
37 | 37 | import uk.me.parabola.mkgmap.osmstyle.eval.OrOp; |
38 | 38 | import uk.me.parabola.mkgmap.osmstyle.eval.RegexOp; |
39 | 39 | import uk.me.parabola.mkgmap.osmstyle.eval.ValueOp; |
40 | import uk.me.parabola.mkgmap.osmstyle.function.StyleFunction; | |
40 | 41 | import uk.me.parabola.mkgmap.scan.SyntaxException; |
41 | 42 | import uk.me.parabola.mkgmap.scan.TokenScanner; |
42 | 43 | |
67 | 68 | Logger log = Logger.getLogger(getClass()); |
68 | 69 | |
69 | 70 | public Op arrange(Op expr) { |
70 | log.debug("IN: " + fmtExpr(expr)); | |
71 | if (log.isDebugEnabled()) | |
72 | log.debug("IN:", fmtExpr(expr)); | |
71 | 73 | Op op = arrangeTop(expr); |
72 | log.debug("OUT: " + fmtExpr(expr)); | |
74 | if (log.isDebugEnabled()) | |
75 | log.debug("OUT:", fmtExpr(expr)); | |
73 | 76 | return op; |
74 | 77 | } |
75 | 78 | |
375 | 378 | private int selectivity(Op op) { |
376 | 379 | // Operations that involve a non-indexable function must always go to the back. |
377 | 380 | if (op.getFirst().isType(FUNCTION)) { |
378 | if (!((ValueOp) op.getFirst()).isIndexable()) | |
379 | return 1000; | |
381 | StyleFunction func = (StyleFunction) op.getFirst(); | |
382 | if (!func.isIndexable()) | |
383 | return 1000 + func.getComplexity(); | |
380 | 384 | } |
381 | 385 | |
382 | 386 | switch (op.getType()) { |
425 | 429 | Op second = op.getSecond(); |
426 | 430 | |
427 | 431 | String keystring = null; |
432 | String valuestring = null; | |
428 | 433 | if (op.isType(EQUALS) && first.isType(FUNCTION) && second.isType(VALUE)) { |
429 | keystring = first.getKeyValue() + "=" + second.getKeyValue(); | |
434 | keystring = first.getKeyValue(); | |
435 | valuestring = second.getKeyValue(); | |
430 | 436 | } else if (op.isType(EXISTS)) { |
431 | keystring = first.getKeyValue() + "=*"; | |
437 | keystring = first.getKeyValue(); | |
438 | valuestring = "*"; | |
432 | 439 | } else if (op.isType(AND)) { |
433 | if (first.isType(EQUALS)) { | |
434 | keystring = first.getFirst().getKeyValue() + "=" + first.getSecond().getKeyValue(); | |
440 | if (first.isType(EQUALS) && first.getFirst().isType(FUNCTION) && first.getSecond().isType(VALUE)) { | |
441 | keystring = first.getFirst().getKeyValue(); | |
442 | valuestring = first.getSecond().getKeyValue(); | |
435 | 443 | } else if (first.isType(EXISTS)) { |
436 | if (!isIndexable(first)) | |
437 | throw new SyntaxException(scanner, "Expression cannot be indexed"); | |
438 | keystring = first.getFirst().getKeyValue() + "=*"; | |
444 | if (isIndexable(first)) { | |
445 | keystring = first.getFirst().getKeyValue(); | |
446 | valuestring = "*"; | |
447 | } | |
439 | 448 | } else if (first.isType(NOT_EXISTS)) { |
440 | throw new SyntaxException(scanner, "Cannot start rule with tag!=*"); | |
449 | // invalid rule | |
441 | 450 | } |
442 | 451 | } |
443 | 452 | |
444 | if (keystring == null) | |
445 | throw new SyntaxException(scanner, "Invalid rule expression: " + op); | |
446 | ||
447 | return keystring; | |
453 | if (keystring == null || valuestring == null) | |
454 | throw new SyntaxException(scanner, "Invalid rule, expression cannot be indexed: " + op); | |
455 | ||
456 | return keystring + "=" + valuestring; | |
448 | 457 | } |
449 | 458 | |
450 | 459 | /** |
115 | 115 | public boolean containsExpression(String exp) { |
116 | 116 | return expression.toString().contains(exp); |
117 | 117 | } |
118 | ||
119 | @Override | |
120 | public void augmentWith(uk.me.parabola.mkgmap.reader.osm.ElementSaver elementSaver) { | |
121 | expression.augmentWith(elementSaver); | |
122 | } | |
123 | ||
118 | 124 | } |
21 | 21 | import java.io.InputStream; |
22 | 22 | import java.io.InputStreamReader; |
23 | 23 | import java.io.Reader; |
24 | import java.io.UnsupportedEncodingException; | |
25 | import java.nio.charset.StandardCharsets; | |
26 | 24 | import java.net.JarURLConnection; |
27 | 25 | import java.net.MalformedURLException; |
28 | 26 | import java.net.URL; |
27 | import java.nio.charset.StandardCharsets; | |
29 | 28 | import java.util.ArrayList; |
30 | 29 | import java.util.Enumeration; |
31 | 30 | import java.util.List; |
311 | 311 | return candidates; |
312 | 312 | } |
313 | 313 | |
314 | @Override | |
315 | public void augmentWith(uk.me.parabola.mkgmap.reader.osm.ElementSaver elementSaver) { | |
316 | if (rules == null) | |
317 | return; | |
318 | for (Rule rule: rules) | |
319 | rule.augmentWith(elementSaver); | |
320 | } | |
314 | 321 | } |
38 | 38 | import java.util.regex.Pattern; |
39 | 39 | |
40 | 40 | import uk.me.parabola.imgfmt.ExitException; |
41 | import uk.me.parabola.imgfmt.Utils; | |
42 | 41 | import uk.me.parabola.log.Logger; |
43 | 42 | import uk.me.parabola.mkgmap.Options; |
44 | 43 | import uk.me.parabola.mkgmap.general.LevelInfo; |
104 | 103 | private OverlayReader overlays; |
105 | 104 | private final boolean performChecks; |
106 | 105 | |
106 | private Collection<String> deadEndTags = new ArrayList<>(); | |
107 | 107 | |
108 | 108 | /** |
109 | 109 | * Create a style from the given location and name. |
129 | 129 | * include the version file being missing. |
130 | 130 | */ |
131 | 131 | public StyleImpl(String loc, String name, EnhancedProperties props, boolean performChecks) throws FileNotFoundException { |
132 | String s = props.getProperty("dead-ends"); | |
133 | if (s != null) { | |
134 | String[] deadEndTagsAndValues = s.split(","); | |
135 | for (String deadEndTag : deadEndTagsAndValues) { | |
136 | deadEndTags.add(deadEndTag.split("=", 2)[0]); | |
137 | } | |
138 | } | |
139 | ||
132 | 140 | location = loc; |
133 | 141 | fileLoader = StyleFileLoader.createStyleLoader(loc, name); |
134 | 142 | this.performChecks = performChecks; |
211 | 219 | set.addAll(lines.getUsedTags()); |
212 | 220 | set.addAll(polygons.getUsedTags()); |
213 | 221 | set.addAll(nodes.getUsedTags()); |
214 | ||
222 | set.addAll(deadEndTags); | |
223 | ||
215 | 224 | // this is to allow style authors to say that tags are really used even |
216 | 225 | // if they are not found in the style file. This is mostly to work |
217 | 226 | // around situations that we haven't thought of - the style is expected |
159 | 159 | private final boolean keepBlanks; |
160 | 160 | |
161 | 161 | private LineAdder lineAdder; |
162 | ||
162 | ||
163 | 163 | public StyledConverter(Style style, MapCollector collector, EnhancedProperties props) { |
164 | 164 | this.collector = collector; |
165 | 165 | |
310 | 310 | |
311 | 311 | if (way.tagIsLikeYes(onewayTagKey)) { |
312 | 312 | way.addTag(onewayTagKey, "yes"); |
313 | if (foundType.isRoad() && checkFixmeCoords(way) ) | |
313 | if (foundType.isRoad() && hasSkipDeadEndCheckNode(way)) | |
314 | 314 | way.addTag("mkgmap:dead-end-check", "false"); |
315 | 315 | } else { |
316 | 316 | way.deleteTag(onewayTagKey); |
387 | 387 | } |
388 | 388 | |
389 | 389 | /** |
390 | * Check if the first or last of the coords of the way has the fixme flag set | |
390 | * Check if the first or last of the coords of the way has a flag set for skipping dead end check | |
391 | 391 | * @param way the way to check |
392 | * @return true if fixme flag was found | |
392 | * @return true if flag was found | |
393 | 393 | */ |
394 | private boolean checkFixmeCoords(Way way) { | |
395 | return way.getFirstPoint().isFixme() || way.getLastPoint().isFixme(); | |
394 | private boolean hasSkipDeadEndCheckNode(Way way) { | |
395 | return way.getFirstPoint().isSkipDeadEndCheck() || way.getLastPoint().isSkipDeadEndCheck(); | |
396 | 396 | } |
397 | 397 | |
398 | 398 | |
695 | 695 | } |
696 | 696 | |
697 | 697 | /** |
698 | * Invoked after the raw OSM data has been read and hooks run, but before any of the convertXxx() calls | |
699 | * | |
700 | * @param elementSaver Gives access to the pre-converted OSM data | |
701 | */ | |
702 | @Override | |
703 | public void augmentWith(uk.me.parabola.mkgmap.reader.osm.ElementSaver elementSaver) { | |
704 | // wayRules doesn't need to be done (or must be done first) because is concat. of line & polygon rules | |
705 | //wayRules.augmentWith(elementSaver); | |
706 | nodeRules.augmentWith(elementSaver); | |
707 | lineRules.augmentWith(elementSaver); | |
708 | polygonRules.augmentWith(elementSaver); | |
709 | } | |
710 | ||
711 | /** | |
698 | 712 | * Built in rules to run after converting the element. |
699 | 713 | */ |
700 | 714 | private static void postConvertRules(Element el, GType type) { |
735 | 749 | for (RestrictionRelation rr : rrList) { |
736 | 750 | if (!rr.isValidWithoutWay(way.getId())) { |
737 | 751 | if (log.isLoggable(logLevel)) { |
738 | log.log(logLevel, "restriction", rr.toBrowseURL(), " is ignored because referenced way", | |
752 | log.log(logLevel, "restriction", rr.toBrowseURL(), "is ignored because referenced way", | |
739 | 753 | way.toBrowseURL(), reason); |
740 | 754 | } |
741 | 755 | rr.setInvalid(); |
966 | 980 | log.info("Found", numRoads, "roads", |
967 | 981 | numDriveOnLeftRoads, "in drive-on-left country,", |
968 | 982 | numDriveOnRightRoads, "in drive-on-right country, and", |
969 | numDriveOnSideUnknown, " with unknwon country"); | |
983 | numDriveOnSideUnknown, " with unknown country"); | |
970 | 984 | if (numDriveOnLeftRoads> 0 && numDriveOnRightRoads > 0) |
971 | 985 | log.error("Attention: Tile contains both drive-on-left (" + numDriveOnLeftRoads + |
972 | 986 | ") and drive-on-right roads (" + numDriveOnRightRoads + ")"); |
65 | 65 | |
66 | 66 | return sb.toString(); |
67 | 67 | } |
68 | ||
69 | @Override | |
70 | public void augmentWith(uk.me.parabola.mkgmap.reader.osm.ElementSaver elementSaver) { | |
71 | first.augmentWith(elementSaver); | |
72 | second.augmentWith(elementSaver); | |
73 | } | |
74 | ||
68 | 75 | } |
216 | 216 | } |
217 | 217 | |
218 | 218 | } |
219 | ||
220 | @Override | |
221 | public void augmentWith(uk.me.parabola.mkgmap.reader.osm.ElementSaver elementSaver) { | |
222 | if (first != null) | |
223 | first.augmentWith(elementSaver); | |
224 | } | |
225 | ||
219 | 226 | } |
14 | 14 | |
15 | 15 | import java.util.Collection; |
16 | 16 | import java.util.Collections; |
17 | import java.util.ArrayList; | |
18 | import java.util.List; | |
17 | 19 | import java.util.HashSet; |
18 | 20 | import java.util.Set; |
19 | 21 | import java.util.Stack; |
26 | 28 | import uk.me.parabola.mkgmap.scan.SyntaxException; |
27 | 29 | import uk.me.parabola.mkgmap.scan.TokenScanner; |
28 | 30 | import uk.me.parabola.mkgmap.scan.WordInfo; |
31 | import uk.me.parabola.mkgmap.scan.Token; | |
32 | import uk.me.parabola.mkgmap.scan.TokType; | |
29 | 33 | |
30 | 34 | import static uk.me.parabola.mkgmap.osmstyle.eval.NodeType.*; |
31 | 35 | |
36 | 40 | public class ExpressionReader { |
37 | 41 | private static final Logger log = Logger.getLogger(ExpressionReader.class); |
38 | 42 | |
39 | private final Stack<Op> stack = new Stack<Op>(); | |
40 | private final Stack<Op> opStack = new Stack<Op>(); | |
43 | private final Stack<Op> stack = new Stack<>(); | |
44 | private final Stack<Op> opStack = new Stack<>(); | |
41 | 45 | private final TokenScanner scanner; |
42 | 46 | private final FeatureKind kind; |
43 | 47 | |
44 | private final Set<String> usedTags = new HashSet<String>(); | |
48 | private final Set<String> usedTags = new HashSet<>(); | |
45 | 49 | |
46 | 50 | public ExpressionReader(TokenScanner scanner, FeatureKind kind) { |
47 | 51 | this.scanner = scanner; |
76 | 80 | pushValue(wordInfo.getText()); |
77 | 81 | } else if (wordInfo.getText().charAt(0) == '$') { |
78 | 82 | String tagname = scanner.nextWord(); |
79 | if (tagname.equals("{")) { | |
83 | if ("{".equals(tagname)) { | |
80 | 84 | tagname = scanner.nextWord(); |
81 | 85 | scanner.validateNext("}"); |
82 | 86 | } |
85 | 89 | // it is a function |
86 | 90 | // this requires a () after the function name |
87 | 91 | scanner.validateNext("("); |
92 | List<String> funcParams = new ArrayList<>(); | |
93 | do { | |
94 | scanner.skipSpace(); | |
95 | Token tok = scanner.peekToken(); | |
96 | if (tok.getType() != TokType.TEXT && | |
97 | (tok.getType() != TokType.SYMBOL || | |
98 | !("'".equals(tok.getValue()) || "\"".equals(tok.getValue())))) | |
99 | break; | |
100 | WordInfo funcParam = scanner.nextWordWithInfo(); | |
101 | funcParams.add(funcParam.getText()); | |
102 | if (scanner.checkToken(",")) { | |
103 | /*Token comma = */scanner.nextToken(); | |
104 | } else { | |
105 | break; | |
106 | } | |
107 | } while (true); | |
88 | 108 | scanner.validateNext(")"); |
89 | saveFunction(wordInfo.getText()); | |
109 | try { | |
110 | saveFunction(wordInfo.getText(), funcParams); | |
111 | } catch (Exception e) { | |
112 | throw new SyntaxException(scanner, e.getMessage()); | |
113 | } | |
90 | 114 | } else { |
91 | 115 | pushValue(wordInfo.getText()); |
92 | 116 | } |
122 | 146 | * @param ifStack |
123 | 147 | * @return |
124 | 148 | */ |
125 | private Op appendIfExpr(Op expr, Collection<Op[]> ifStack) { | |
149 | private static Op appendIfExpr(Op expr, Collection<Op[]> ifStack) { | |
126 | 150 | Op result = expr; |
127 | 151 | for (Op[] ops : ifStack) { |
128 | 152 | if (result != null) { |
130 | 154 | and.setFirst(result); |
131 | 155 | and.setSecond(ops[0].copy()); |
132 | 156 | result = and; |
133 | } else | |
157 | } else { | |
134 | 158 | result = ops[0].copy(); |
135 | ||
159 | } | |
136 | 160 | } |
137 | 161 | return result; |
138 | 162 | } |
142 | 166 | * @param token The string to test. |
143 | 167 | * @return True if this looks like an operator. |
144 | 168 | */ |
145 | private boolean isOperation(WordInfo token) { | |
169 | private static boolean isOperation(WordInfo token) { | |
146 | 170 | // A quoted word is not an operator eg: '=' is a string. |
147 | 171 | if (token.isQuoted()) |
148 | 172 | return false; |
173 | 197 | */ |
174 | 198 | private void saveOp(String value) { |
175 | 199 | log.debug("save op", value); |
176 | if (value.equals("#")) { | |
200 | if ("#".equals(value)) { | |
177 | 201 | scanner.skipLine(); |
178 | 202 | return; |
179 | 203 | } |
252 | 276 | op.setFirst(arg1); |
253 | 277 | } |
254 | 278 | } else if (!op.isType(OPEN_PAREN)) { |
255 | if (stack.size() < 1) | |
279 | if (stack.isEmpty()) | |
256 | 280 | throw new SyntaxException(scanner, String.format("Missing argument for %s operator", |
257 | 281 | op.getType().toSymbol())); |
258 | 282 | op.setFirst(stack.pop()); |
276 | 300 | * |
277 | 301 | * @param functionName A name to look up. |
278 | 302 | */ |
279 | private void saveFunction(String functionName) { | |
303 | private void saveFunction(String functionName, List<String> functionParams) { | |
280 | 304 | StyleFunction function = FunctionFactory.createFunction(functionName); |
281 | 305 | if (function == null) |
282 | 306 | throw new SyntaxException(String.format("No function with name '%s()'", functionName)); |
307 | function.setParams(functionParams, kind); | |
283 | 308 | |
284 | 309 | // TODO: supportsWay split into supportsPoly{line,gon}, or one function supports(kind) |
285 | 310 | boolean supported = false; |
303 | 328 | |
304 | 329 | if (!supported) |
305 | 330 | throw new SyntaxException(String.format("Function '%s()' not supported for %s", functionName, kind)); |
306 | ||
331 | usedTags.addAll(function.getUsedTags()); | |
307 | 332 | stack.push(function); |
308 | 333 | } |
309 | 334 |
168 | 168 | public Set<String> getEvaluatedTagKeys() { |
169 | 169 | return wrapped.getEvaluatedTagKeys(); |
170 | 170 | } |
171 | ||
172 | @Override | |
173 | public void augmentWith(uk.me.parabola.mkgmap.reader.osm.ElementSaver elementSaver) { | |
174 | if (wrapped != null) | |
175 | wrapped.augmentWith(elementSaver); | |
176 | if (link != null) | |
177 | link.augmentWith(elementSaver); | |
178 | } | |
179 | ||
171 | 180 | } |
34 | 34 | * @param el The OSM element to be tested. |
35 | 35 | * @return True if the expression is true for the given element. |
36 | 36 | */ |
37 | public boolean eval(Element el); | |
37 | boolean eval(Element el); | |
38 | 38 | |
39 | 39 | /** |
40 | 40 | * Evaluate the expression using a cache. |
42 | 42 | * @param el The OSM element to be tested. |
43 | 43 | * @return True if the expression is true for the given element. |
44 | 44 | */ |
45 | public boolean eval(int cacheId, Element el); | |
45 | boolean eval(int cacheId, Element el); | |
46 | 46 | |
47 | 47 | |
48 | 48 | /** |
49 | 49 | * Does this operation have a higher priority that the other one? |
50 | 50 | * @param other The other operation. |
51 | 51 | */ |
52 | public boolean hasHigherPriority(Op other); | |
52 | boolean hasHigherPriority(Op other); | |
53 | 53 | |
54 | 54 | /** |
55 | 55 | * Get the first operand. |
56 | 56 | */ |
57 | public Op getFirst(); | |
57 | Op getFirst(); | |
58 | 58 | |
59 | 59 | /** |
60 | 60 | * Set the first operand. |
61 | 61 | */ |
62 | public <T extends Op> T setFirst(Op first); | |
62 | <T extends Op> T setFirst(Op first); | |
63 | 63 | |
64 | 64 | /** |
65 | 65 | * Set the second operand. |
66 | 66 | * Only supported on binary nodes, but declared here to avoid casts all over the place. |
67 | 67 | */ |
68 | public default void setSecond(Op second) { | |
68 | default void setSecond(Op second) { | |
69 | 69 | throw new UnsupportedOperationException("setSecond only supported on binary nodes"); |
70 | 70 | } |
71 | 71 | |
74 | 74 | * then null is returned. |
75 | 75 | * @return The right hand side, or null if there is not one. |
76 | 76 | */ |
77 | public Op getSecond(); | |
77 | Op getSecond(); | |
78 | 78 | |
79 | 79 | /** |
80 | 80 | * Set both first and second in one call. |
81 | 81 | * |
82 | 82 | * Only supported on BinaryOp types. |
83 | 83 | */ |
84 | public <T extends Op> T set(Op first, Op second); | |
84 | <T extends Op> T set(Op first, Op second); | |
85 | 85 | |
86 | 86 | /** Get the operation type */ |
87 | public NodeType getType(); | |
87 | NodeType getType(); | |
88 | 88 | |
89 | 89 | /** |
90 | 90 | * For operations that are value types this is the string value. |
94 | 94 | * |
95 | 95 | * @return The value, or UnsupportedOperationException if it does not have a value. |
96 | 96 | */ |
97 | public String value(Element el); | |
97 | String value(Element el); | |
98 | 98 | |
99 | 99 | /** |
100 | 100 | * For a value-type node, this is a key value associated with value. For a base Value node |
101 | 101 | * this is the same as value(), but if value() is overridden then it may not be. |
102 | 102 | */ |
103 | public String getKeyValue(); | |
103 | String getKeyValue(); | |
104 | 104 | |
105 | 105 | /** |
106 | 106 | * Test the node type and return true if it matches the given argument. |
107 | 107 | */ |
108 | public boolean isType(NodeType value); | |
108 | boolean isType(NodeType value); | |
109 | 109 | |
110 | 110 | /** |
111 | 111 | * For an operation this is a number that determines the precedence of this operation. |
112 | 112 | * Used when building the node tree. Higher numbers bind more tightly. |
113 | 113 | */ |
114 | public int priority(); | |
114 | int priority(); | |
115 | 115 | |
116 | 116 | /** |
117 | 117 | * @return a set with the tag keys which are evaluated, maybe empty |
118 | 118 | */ |
119 | public Set<String> getEvaluatedTagKeys(); | |
119 | Set<String> getEvaluatedTagKeys(); | |
120 | 120 | |
121 | 121 | |
122 | 122 | /** |
126 | 126 | * they are never modified by the arranger code. If you are using this for something else |
127 | 127 | * you may need a different method. |
128 | 128 | */ |
129 | public default Op copy() { | |
129 | default Op copy() { | |
130 | 130 | NodeType t = getType(); |
131 | 131 | if (t == AND || t == OR) { |
132 | 132 | return AbstractOp.createOp(getType()) |
134 | 134 | } else |
135 | 135 | return this; |
136 | 136 | } |
137 | ||
138 | default void augmentWith(uk.me.parabola.mkgmap.reader.osm.ElementSaver elementSaver) {} | |
139 | ||
137 | 140 | } |
3 | 3 | |
4 | 4 | import uk.me.parabola.imgfmt.app.Coord; |
5 | 5 | import uk.me.parabola.mkgmap.reader.osm.Element; |
6 | import uk.me.parabola.mkgmap.reader.osm.MultiPolygonRelation; | |
7 | 6 | import uk.me.parabola.mkgmap.reader.osm.Way; |
8 | 7 | |
9 | 8 | /** |
21 | 20 | */ |
22 | 21 | public class AreaSizeFunction extends CachedFunction { |
23 | 22 | |
24 | private final boolean orderByDecreasingArea = true; | |
25 | ||
26 | 23 | public AreaSizeFunction() { |
27 | 24 | super(null); |
28 | 25 | } |
31 | 28 | if (el instanceof Way) { |
32 | 29 | Way w = (Way)el; |
33 | 30 | // a non closed way has size 0 |
34 | if (w.hasEqualEndPoints() == false) { | |
31 | if (!w.hasEqualEndPoints()) { | |
35 | 32 | return "0"; |
36 | 33 | } |
37 | double areaSize; | |
38 | if (orderByDecreasingArea) { | |
39 | long fullArea = w.getFullArea(); | |
40 | if (fullArea == Long.MAX_VALUE) | |
41 | return "0"; | |
42 | // convert from high prec to value in map units | |
43 | areaSize = (double) fullArea / (2 * (1<<Coord.DELTA_SHIFT) * (1<<Coord.DELTA_SHIFT)); | |
44 | areaSize = Math.abs(areaSize); | |
45 | } else | |
46 | areaSize = MultiPolygonRelation.calcAreaSize(w.getPoints()); | |
47 | return String.format(Locale.US, "%.3f", areaSize); | |
34 | long fullArea = w.getFullArea(); | |
35 | if (fullArea == Long.MAX_VALUE) | |
36 | return "0"; | |
37 | // convert from high prec to value in map units | |
38 | double areaSize = (double) fullArea / (2 * (1<<Coord.DELTA_SHIFT) * (1<<Coord.DELTA_SHIFT)); | |
39 | return String.format(Locale.US, "%.3f", Math.abs(areaSize)); | |
48 | 40 | } |
49 | 41 | return null; |
50 | 42 | } |
51 | 43 | |
44 | @Override | |
52 | 45 | public String getName() { |
53 | 46 | return "area_size"; |
54 | 47 | } |
55 | 48 | |
49 | @Override | |
56 | 50 | public boolean supportsWay() { |
57 | 51 | return true; |
58 | 52 | } |
59 | 53 | |
54 | @Override | |
55 | public int getComplexity() { | |
56 | return 2; | |
57 | } | |
60 | 58 | } |
47 | 47 | return new TypeFunction(); |
48 | 48 | if ("osmid".equals(name)) |
49 | 49 | return new OsmIdFunction(); |
50 | if ("is_in".equals(name)) | |
51 | return new IsInFunction(); | |
50 | 52 | |
51 | 53 | return null; |
52 | 54 | } |
0 | /* | |
1 | * Copyright (C) 2019. | |
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 | ||
13 | package uk.me.parabola.mkgmap.osmstyle.function; | |
14 | ||
15 | import java.util.ArrayList; | |
16 | import java.util.Collections; | |
17 | import java.util.LinkedHashSet; | |
18 | import java.util.List; | |
19 | import java.util.Set; | |
20 | import java.util.stream.Collectors; | |
21 | ||
22 | import uk.me.parabola.imgfmt.ExitException; | |
23 | import uk.me.parabola.imgfmt.app.Area; | |
24 | import uk.me.parabola.imgfmt.app.Coord; | |
25 | import uk.me.parabola.log.Logger; | |
26 | import uk.me.parabola.mkgmap.reader.osm.Element; | |
27 | import uk.me.parabola.mkgmap.reader.osm.ElementSaver; | |
28 | import uk.me.parabola.mkgmap.reader.osm.FeatureKind; | |
29 | import uk.me.parabola.mkgmap.reader.osm.MultiPolygonRelation; | |
30 | import uk.me.parabola.mkgmap.reader.osm.Node; | |
31 | import uk.me.parabola.mkgmap.reader.osm.Way; | |
32 | import uk.me.parabola.mkgmap.scan.SyntaxException; | |
33 | import uk.me.parabola.util.ElementQuadTree; | |
34 | import uk.me.parabola.util.IsInUtil; | |
35 | ||
36 | /** | |
37 | * | |
38 | * @author Ticker Berkin | |
39 | * | |
40 | */ | |
41 | public class IsInFunction extends CachedFunction { // StyleFunction | |
42 | private static final Logger log = Logger.getLogger(IsInFunction.class); | |
43 | ||
44 | private enum MethodArg { | |
45 | ||
46 | // can stop when: IN ON OUT MERGE | |
47 | POINT_IN("in", FeatureKind.POINT, true, false, false, true) | |
48 | { @Override public boolean mapFlags(boolean hasIn, boolean hasOn, boolean hasOut) {return hasIn;} }, | |
49 | POINT_IN_OR_ON("in_or_on", FeatureKind.POINT, true, true, false, false) | |
50 | { @Override public boolean mapFlags(boolean hasIn, boolean hasOn, boolean hasOut) {return hasIn || hasOn;} }, | |
51 | POINT_ON("on", FeatureKind.POINT, false, true, false, true) | |
52 | { @Override public boolean mapFlags(boolean hasIn, boolean hasOn, boolean hasOut) {return hasOn;} }, | |
53 | ||
54 | LINE_SOME_IN_NONE_OUT("all", FeatureKind.POLYLINE, false, false, true, true) | |
55 | { @Override public boolean mapFlags(boolean hasIn, boolean hasOn, boolean hasOut) {return hasIn && !hasOut;} }, | |
56 | LINE_ALL_IN_OR_ON("all_in_or_on", FeatureKind.POLYLINE, false, false, true, true) | |
57 | { @Override public boolean mapFlags(boolean hasIn, boolean hasOn, boolean hasOut) {return !hasOut;} }, | |
58 | LINE_ALL_ON("on", FeatureKind.POLYLINE, true, false, true, true) | |
59 | { @Override public boolean mapFlags(boolean hasIn, boolean hasOn, boolean hasOut) {return !(hasIn || hasOut);} }, | |
60 | LINE_ANY_IN("any", FeatureKind.POLYLINE, true, false, false, true) | |
61 | { @Override public boolean mapFlags(boolean hasIn, boolean hasOn, boolean hasOut) {return hasIn;} }, | |
62 | // LINE_ANY_IN_OR_ON("any_in_or_on", FeatureKind.POLYLINE, true, false, false, true) | |
63 | // { @Override public boolean mapFlags(boolean hasIn, boolean hasOn, boolean hasOut) {return hasIn || !hasOut;} }, | |
64 | LINE_NONE_IN_SOME_OUT("none", FeatureKind.POLYLINE, true, false, false, true) | |
65 | { @Override public boolean mapFlags(boolean hasIn, boolean hasOn, boolean hasOut) {return !hasIn && hasOut;} }, | |
66 | ||
67 | POLYGON_ALL("all", FeatureKind.POLYGON, false, false, true, true) | |
68 | { @Override public boolean mapFlags(boolean hasIn, boolean hasOn, boolean hasOut) {return !hasOut;} }, | |
69 | POLYGON_ANY("any", FeatureKind.POLYGON, true, false, false, false) | |
70 | { @Override public boolean mapFlags(boolean hasIn, boolean hasOn, boolean hasOut) {return hasIn || !hasOut;} }; | |
71 | ||
72 | /* thoughts for ON methods for polyons and the hasOn flag | |
73 | ||
74 | possible methods: | |
75 | on_outer / on | |
76 | on_inner / hole | |
77 | on_either | |
78 | all_or_inner - to match, say building, even when cut out of area | |
79 | ||
80 | on_outer is ok, with just ON. | |
81 | on_inner would be logical to represent as ON|OUT | |
82 | but, at the moment, an outside line/poly touching an outer will also set this combination | |
83 | ||
84 | Could: | |
85 | don't hasOn() when isLineInShape returns IN|ON|OUT (in setHasFromFlags) | |
86 | other places where currently call hasOn(), test kind for poly and don't when in comb. with IN or OUT | |
87 | ||
88 | actually, would be safe not to call hasOn() even for POLYLINE, because none of the methods test it | |
89 | */ | |
90 | ||
91 | public abstract boolean mapFlags(boolean hasIn, boolean hasOn, boolean hasOut); | |
92 | ||
93 | private final String methodName; | |
94 | private final FeatureKind kind; | |
95 | private final boolean stopIn; | |
96 | private final boolean stopOn; | |
97 | private final boolean stopOut; | |
98 | private final boolean needMerge; | |
99 | ||
100 | MethodArg(String methodName, FeatureKind kind, boolean stopIn, boolean stopOn, boolean stopOut, boolean needMerge) { | |
101 | this.methodName = methodName; | |
102 | this.kind = kind; | |
103 | this.stopIn = stopIn; | |
104 | this.stopOn = stopOn; | |
105 | this.stopOut = stopOut; | |
106 | this.needMerge = needMerge; | |
107 | } | |
108 | ||
109 | @Override | |
110 | public String toString() { | |
111 | return methodName; | |
112 | } | |
113 | ||
114 | public FeatureKind getKind() { | |
115 | return kind; | |
116 | } | |
117 | ||
118 | public boolean canStopIn() { | |
119 | return stopIn; | |
120 | } | |
121 | public boolean canStopOn() { | |
122 | return stopOn; | |
123 | } | |
124 | public boolean canStopOut() { | |
125 | return stopOut; | |
126 | } | |
127 | public boolean needMerge() { | |
128 | return needMerge; | |
129 | } | |
130 | } | |
131 | ||
132 | private class CanStopProcessing extends RuntimeException {} | |
133 | ||
134 | private MethodArg method; | |
135 | private boolean hasIn; | |
136 | private boolean hasOn; | |
137 | private boolean hasOut; | |
138 | private ElementQuadTree qt = null; | |
139 | ||
140 | public IsInFunction() { | |
141 | super(null); | |
142 | reqdNumParams = 3; | |
143 | // 1: polygon tagName | |
144 | // 2: value for above tag | |
145 | // 3: method keyword, see above | |
146 | log.debug("isInFunction", System.identityHashCode(this)); | |
147 | } | |
148 | ||
149 | private void resetHasFlags() { | |
150 | // the instance is per unique call in rules, then applied repeatedly to each point/line/polygon | |
151 | hasIn = false; | |
152 | hasOn = false; | |
153 | hasOut = false; | |
154 | } | |
155 | ||
156 | public String calcImpl(Element el) { | |
157 | log.debug("calcImpl", System.identityHashCode(this), kind, params, el); | |
158 | assert qt != null : "invoked the non-augmented instance"; | |
159 | if (qt.isEmpty()) | |
160 | return String.valueOf(false); | |
161 | resetHasFlags(); | |
162 | try { | |
163 | switch (kind) { | |
164 | case POINT: | |
165 | doPointTest((Node) el); | |
166 | break; | |
167 | case POLYLINE: | |
168 | doLineTest((Way) el); | |
169 | break; | |
170 | case POLYGON: | |
171 | doPolygonTest((Way) el); | |
172 | break; | |
173 | default: | |
174 | throw new ExitException("Bad FeatureKind: " + kind); | |
175 | } | |
176 | } catch (CanStopProcessing e) {} | |
177 | log.debug("done", System.identityHashCode(this), hasIn, hasOn, hasOut); | |
178 | if (!hasIn && !hasOn) | |
179 | hasOut = true; | |
180 | return String.valueOf(method.mapFlags(hasIn, hasOn, hasOut)); | |
181 | } | |
182 | ||
183 | /* don't have this for CachedFunction | |
184 | @Override | |
185 | public String value(Element el) { | |
186 | return calcImpl(el); | |
187 | } | |
188 | */ | |
189 | ||
190 | @Override | |
191 | public void setParams(List<String> params, FeatureKind kind) { | |
192 | super.setParams(params, kind); | |
193 | log.debug("setParams", System.identityHashCode(this), kind, params); | |
194 | String methodStr = params.get(2); | |
195 | boolean knownMethod = false; | |
196 | List<String> methodsForKind = new ArrayList<>(); | |
197 | for (MethodArg tstMethod : MethodArg.values()) { | |
198 | if (methodStr.equalsIgnoreCase(tstMethod.toString())) { | |
199 | if (tstMethod.getKind() == kind) { | |
200 | this.method = tstMethod; | |
201 | return; | |
202 | } else { | |
203 | knownMethod = true; | |
204 | } | |
205 | } else if (tstMethod.getKind() == kind) { | |
206 | methodsForKind.add(tstMethod.toString()); | |
207 | } | |
208 | } | |
209 | throw new SyntaxException(String.format("Third parameter '%s' of function %s is not " + | |
210 | (knownMethod ? "supported for this style section" : "understood") + | |
211 | ", valid are: %s" , methodStr, getName(), methodsForKind)); | |
212 | } | |
213 | ||
214 | private void setIn() { | |
215 | log.debug("setIn", hasIn, hasOn, hasOut); | |
216 | hasIn = true; | |
217 | if (method.canStopIn() || hasOut) | |
218 | throw new CanStopProcessing(); | |
219 | } | |
220 | ||
221 | private void setOn() { | |
222 | log.debug("setOn", hasIn, hasOn, hasOut); | |
223 | hasOn = true; | |
224 | if (method.canStopOn() || (hasIn && hasOut)) | |
225 | throw new CanStopProcessing(); | |
226 | } | |
227 | private void setOut() { | |
228 | log.debug("setOut", hasIn, hasOn, hasOut); | |
229 | hasOut = true; | |
230 | if (method.canStopOut() || hasIn) | |
231 | throw new CanStopProcessing(); | |
232 | } | |
233 | ||
234 | private void setHasFromFlags(int flags) { | |
235 | log.debug("setFlags", flags); | |
236 | if ((flags & IsInUtil.ON) != 0) | |
237 | setOn(); | |
238 | if ((flags & IsInUtil.IN) != 0) | |
239 | setIn(); | |
240 | if ((flags & IsInUtil.OUT) != 0) | |
241 | setOut(); | |
242 | } | |
243 | ||
244 | private static boolean notInHole(Coord c, List<List<Coord>> holes) { | |
245 | if (holes == null) | |
246 | return true; | |
247 | for (List<Coord> hole : holes) { | |
248 | int flags = IsInUtil.isPointInShape(c, hole); | |
249 | log.debug("notInHole", flags); | |
250 | if (flags != IsInUtil.OUT) | |
251 | return false; | |
252 | } | |
253 | return true; | |
254 | } | |
255 | ||
256 | private void checkPointInShape(Coord c, List<Coord> shape, List<List<Coord>> holes) { | |
257 | /* | |
258 | Because we are processing polygons one-by-one, OUT is only meaningful once we have | |
259 | checked all the polygons and haven't satisfied IN/ON, so no point is calling setOut() | |
260 | and it wouldn't stop the processing or effect the answer anyway | |
261 | */ | |
262 | int flags = IsInUtil.isPointInShape(c, shape); | |
263 | log.debug("checkPoint", flags); | |
264 | switch (method) { | |
265 | case POINT_IN: | |
266 | if (flags == IsInUtil.IN) { | |
267 | if (notInHole(c, holes)) { | |
268 | setIn(); | |
269 | } else { | |
270 | // in hole in this shape, no point in looking at more shapes | |
271 | throw new CanStopProcessing(); | |
272 | } | |
273 | } | |
274 | break; | |
275 | case POINT_IN_OR_ON: | |
276 | if (flags != IsInUtil.OUT) | |
277 | // no need to check holes for this as didn't need to merge polygons | |
278 | setIn(); // don't care about setOn() | |
279 | break; | |
280 | case POINT_ON: | |
281 | if (flags == IsInUtil.ON) | |
282 | // hole checking is a separate pass | |
283 | setOn(); // don't care about setIn() | |
284 | break; | |
285 | default: | |
286 | throw new ExitException("Bad point method: " + method); | |
287 | } | |
288 | } | |
289 | ||
290 | private void doPointTest(Node el) { | |
291 | Coord c = el.getLocation(); | |
292 | Area elementBbox = Area.getBBox(Collections.singletonList(c)); | |
293 | Set<Way> polygons = qt.get(elementBbox).stream().map(e -> (Way) e) | |
294 | .collect(Collectors.toCollection(LinkedHashSet::new)); | |
295 | if (method.needMerge() && polygons.size() > 1) { | |
296 | // need to merge shapes so that POI on shared boundary becomes IN rather than ON | |
297 | List<List<Coord>> outers = new ArrayList<>(); | |
298 | List<List<Coord>> holes = new ArrayList<>(); | |
299 | IsInUtil.mergePolygons(polygons, outers, holes); | |
300 | log.debug("pointMerge", polygons.size(), outers.size(), holes.size()); | |
301 | for (List<Coord> shape : outers) | |
302 | checkPointInShape(c, shape, holes); | |
303 | if (method == MethodArg.POINT_ON && !holes.isEmpty()) | |
304 | // need to check if on edge of hole | |
305 | for (List<Coord> hole : holes) | |
306 | checkPointInShape(c, hole, null); | |
307 | } else { // just one polygon or IN_OR_ON, which can do one-by-one | |
308 | log.debug("point1by1", polygons.size()); | |
309 | for (Way polygon : polygons) | |
310 | checkPointInShape(c, polygon.getPoints(), null); | |
311 | } | |
312 | } | |
313 | ||
314 | private void doLineTest(Way el) { | |
315 | doCommonTest(el); | |
316 | } | |
317 | ||
318 | private void doPolygonTest(Way el) { | |
319 | doCommonTest(el); | |
320 | } | |
321 | ||
322 | private boolean checkHoles(List<Coord> polyLine, List<List<Coord>> holes, Area elementBbox) { | |
323 | boolean foundSomething = false; | |
324 | for (List<Coord> hole : holes) { | |
325 | int flags = IsInUtil.isLineInShape(polyLine, hole, elementBbox); | |
326 | log.debug("checkhole", flags); | |
327 | if ((flags & IsInUtil.IN) != 0) { | |
328 | setOut(); | |
329 | if ((flags & IsInUtil.ON) != 0) | |
330 | setOn(); | |
331 | if ((flags & IsInUtil.OUT) != 0) | |
332 | setIn(); | |
333 | return true; | |
334 | } else if ((flags & IsInUtil.ON) != 0) { | |
335 | setOn(); | |
336 | if ((flags & IsInUtil.OUT) != 0) | |
337 | setIn(); | |
338 | foundSomething = true; | |
339 | } | |
340 | } | |
341 | return foundSomething; | |
342 | } | |
343 | ||
344 | private void checkHoleInThis(List<Coord> polyLine, List<List<Coord>> holes, Area elementBbox) { | |
345 | for (List<Coord> hole : holes) { | |
346 | int flags = IsInUtil.isLineInShape(hole, polyLine, elementBbox); | |
347 | log.debug("holeInThis", flags); | |
348 | if ((flags & IsInUtil.IN) != 0 || | |
349 | (flags == IsInUtil.ON)) { // exactly on hole | |
350 | setOut(); | |
351 | return; | |
352 | } | |
353 | } | |
354 | } | |
355 | ||
356 | private void doCommonTest(Element el) { | |
357 | List<Coord> polyLine = ((Way)el).getPoints(); | |
358 | Area elementBbox = Area.getBBox(polyLine); | |
359 | Set<Way> polygons = qt.get(elementBbox).stream().map(e -> (Way) e) | |
360 | .collect(Collectors.toCollection(LinkedHashSet::new)); | |
361 | if (log.isDebugEnabled()) { | |
362 | log.debug("line", polyLine); | |
363 | log.debug(polygons.size(), "polygons"); | |
364 | for (Way polygon : polygons) | |
365 | log.debug("polygon", polygon.getPoints()); | |
366 | } | |
367 | if (method.needMerge() && polygons.size() > 1) { // ALL-like methods need to merge shapes | |
368 | List<List<Coord>> outers = new ArrayList<>(); | |
369 | List<List<Coord>> holes = new ArrayList<>(); | |
370 | IsInUtil.mergePolygons(polygons, outers, holes); | |
371 | if (log.isDebugEnabled()) { | |
372 | log.debug(outers.size(), "outers", holes.size(), "holes"); | |
373 | for (List<Coord> shape : outers) | |
374 | log.debug("outer", shape); | |
375 | for (List<Coord> hole : holes) | |
376 | log.debug("hole", hole); | |
377 | } | |
378 | for (List<Coord> shape : outers) { | |
379 | int flags = IsInUtil.isLineInShape(polyLine, shape, elementBbox); | |
380 | log.debug("checkShape", flags); | |
381 | if ((flags & IsInUtil.IN) != 0) { // this shape is the one to consider | |
382 | if ((flags & IsInUtil.ON) != 0) | |
383 | setOn(); | |
384 | if ((flags & IsInUtil.OUT) != 0) | |
385 | setOut(); | |
386 | if (!checkHoles(polyLine, holes, elementBbox)) | |
387 | setIn(); | |
388 | if (!hasOut && kind == FeatureKind.POLYGON) | |
389 | checkHoleInThis(polyLine, holes, elementBbox); | |
390 | break; | |
391 | } else if ((flags & IsInUtil.ON) != 0) { // might still be IN later one | |
392 | setOn(); | |
393 | if ((flags & IsInUtil.OUT) != 0) | |
394 | setOut(); | |
395 | else { // exactly on | |
396 | if (kind == FeatureKind.POLYGON) | |
397 | checkHoleInThis(polyLine, holes, elementBbox); | |
398 | break; // hence can't be in another | |
399 | } | |
400 | } | |
401 | } | |
402 | } else { // an ANY-like method or 1 polygon | |
403 | for (Way polygon : polygons) | |
404 | setHasFromFlags(IsInUtil.isLineInShape(polyLine, polygon.getPoints(), elementBbox)); | |
405 | } | |
406 | } | |
407 | ||
408 | @Override | |
409 | public String getName() { | |
410 | return "is_in"; | |
411 | } | |
412 | ||
413 | @Override | |
414 | public boolean supportsNode() { | |
415 | return true; | |
416 | } | |
417 | ||
418 | @Override | |
419 | public boolean supportsWay() { | |
420 | return true; | |
421 | } | |
422 | ||
423 | @Override | |
424 | public Set<String> getUsedTags() { | |
425 | return Collections.singleton(params.get(0)); | |
426 | } | |
427 | ||
428 | @Override | |
429 | public String toString() { | |
430 | // see RuleSet.compile() | |
431 | return getName() + "(" + kind + ", " + String.join(", ", params) + ")"; | |
432 | } | |
433 | ||
434 | @Override | |
435 | protected String getCacheTag() { | |
436 | return "mkgmap:cache_is_in_" + kind + "_" + String.join("_", params); | |
437 | } | |
438 | ||
439 | @Override | |
440 | public void augmentWith(ElementSaver elementSaver) { | |
441 | log.debug("augmentWith", System.identityHashCode(this), kind, params); | |
442 | // the cached function mechanism creates an instance for each occurance in the rule file | |
443 | // but then just uses one of them for augmentWith() and calcImpl(). | |
444 | if (qt != null) | |
445 | return; | |
446 | qt = buildTree(elementSaver, params.get(0), params.get(1)); | |
447 | } | |
448 | ||
449 | public static ElementQuadTree buildTree(ElementSaver elementSaver, String tagKey, String tagVal) { | |
450 | List<Element> matchingPolygons = new ArrayList<>(); | |
451 | for (Way w : elementSaver.getWays().values()) { | |
452 | if (w.hasIdenticalEndPoints() | |
453 | && !"polyline".equals(w.getTag(MultiPolygonRelation.STYLE_FILTER_TAG))) { | |
454 | String val = w.getTag(tagKey); | |
455 | if (val != null && val.equals(tagVal)) { | |
456 | matchingPolygons.add(w); | |
457 | } | |
458 | } | |
459 | } | |
460 | return new ElementQuadTree(elementSaver.getBoundingBox(), matchingPolygons); | |
461 | } | |
462 | ||
463 | public void unitTestAugment(ElementQuadTree qt) { | |
464 | this.qt = qt; | |
465 | } | |
466 | ||
467 | @Override | |
468 | public int getComplexity() { | |
469 | return 5; | |
470 | } | |
471 | } |
79 | 79 | public boolean supportsRelation() { |
80 | 80 | return true; |
81 | 81 | } |
82 | ||
83 | @Override | |
84 | public int getComplexity() { | |
85 | return 2; | |
86 | } | |
82 | 87 | } |
14 | 14 | |
15 | 15 | import java.text.DecimalFormat; |
16 | 16 | import java.text.DecimalFormatSymbols; |
17 | import java.util.Collections; | |
17 | 18 | import java.util.Locale; |
19 | import java.util.Set; | |
18 | 20 | import java.util.regex.Pattern; |
19 | 21 | |
20 | 22 | import uk.me.parabola.mkgmap.reader.osm.Element; |
95 | 97 | |
96 | 98 | } |
97 | 99 | |
100 | @Override | |
98 | 101 | public String getName() { |
99 | 102 | switch (this.unit) { |
100 | 103 | case MPH: |
105 | 108 | } |
106 | 109 | } |
107 | 110 | |
111 | @Override | |
108 | 112 | public boolean supportsWay() { |
109 | 113 | return true; |
110 | 114 | } |
115 | ||
116 | @Override | |
117 | public Set<String> getUsedTags() { | |
118 | return Collections.singleton("maxspeed"); | |
119 | } | |
111 | 120 | } |
12 | 12 | |
13 | 13 | package uk.me.parabola.mkgmap.osmstyle.function; |
14 | 14 | |
15 | import static uk.me.parabola.mkgmap.osmstyle.eval.NodeType.FUNCTION; | |
16 | ||
17 | import java.util.ArrayList; | |
18 | import java.util.Collections; | |
19 | import java.util.List; | |
20 | import java.util.Set; | |
21 | ||
15 | 22 | import uk.me.parabola.mkgmap.osmstyle.eval.ValueOp; |
23 | import uk.me.parabola.mkgmap.reader.osm.FeatureKind; | |
16 | 24 | import uk.me.parabola.mkgmap.reader.osm.Node; |
17 | 25 | import uk.me.parabola.mkgmap.reader.osm.Relation; |
18 | 26 | import uk.me.parabola.mkgmap.reader.osm.Way; |
19 | ||
20 | import static uk.me.parabola.mkgmap.osmstyle.eval.NodeType.FUNCTION; | |
27 | import uk.me.parabola.mkgmap.scan.SyntaxException; | |
21 | 28 | |
22 | 29 | /** |
23 | 30 | * The interface for all functions that can be used within a style file.<br> |
27 | 34 | */ |
28 | 35 | public abstract class StyleFunction extends ValueOp { |
29 | 36 | |
37 | protected int reqdNumParams = 0; | |
38 | protected List<String> params; | |
39 | protected FeatureKind kind; | |
40 | ||
30 | 41 | public StyleFunction(String value) { |
31 | 42 | super(value); |
32 | 43 | setType(FUNCTION); |
44 | } | |
45 | ||
46 | public void setParams(List<String> params, FeatureKind kind) { | |
47 | if (params.size() != reqdNumParams) | |
48 | throw new SyntaxException(String.format("Function %s takes %d parameters, %d given", getName(), reqdNumParams, params.size())); | |
49 | this.params = new ArrayList<>(params); | |
50 | this.kind = kind; | |
33 | 51 | } |
34 | 52 | |
35 | 53 | /** |
69 | 87 | return getKeyValue(); |
70 | 88 | } |
71 | 89 | |
90 | @Override | |
72 | 91 | public String toString() { |
73 | 92 | return getName() + "()"; |
74 | 93 | } |
94 | ||
95 | /** | |
96 | * @return the tag keys evaluated in this function. | |
97 | */ | |
98 | public Set<String> getUsedTags() { | |
99 | return Collections.emptySet(); | |
100 | } | |
101 | ||
102 | /** | |
103 | * | |
104 | * @return an estimate for the complexity of this function, a value >= 1 and <= 10 | |
105 | */ | |
106 | public int getComplexity() { | |
107 | return 1; | |
108 | } | |
75 | 109 | } |
401 | 401 | Coord c = c1.makeBetweenPoint(c2, fraction); |
402 | 402 | interpolated.add(c); |
403 | 403 | if (interpolated.size() >= steps){ |
404 | // GpxCreator.createGpx("e:/ld/road", knownHouses[0].getRoad().getPoints()); | |
405 | // GpxCreator.createGpx("e:/ld/test", interpolated, Arrays.asList(points.get(0),points.get(points.size()-1))); | |
406 | 404 | return interpolated; |
407 | 405 | } |
408 | 406 | rest = 0; |
15 | 15 | */ |
16 | 16 | package uk.me.parabola.mkgmap.reader; |
17 | 17 | |
18 | import java.io.File; | |
19 | import java.nio.charset.StandardCharsets; | |
20 | import java.nio.file.Files; | |
21 | import java.time.LocalDateTime; | |
22 | import java.time.format.DateTimeFormatter; | |
23 | import java.time.format.FormatStyle; | |
24 | import java.util.ArrayList; | |
18 | 25 | import java.util.List; |
26 | import java.util.function.UnaryOperator; | |
19 | 27 | |
28 | import uk.me.parabola.imgfmt.ExitException; | |
20 | 29 | import uk.me.parabola.imgfmt.app.Area; |
21 | 30 | import uk.me.parabola.imgfmt.app.net.RoadNetwork; |
22 | 31 | import uk.me.parabola.imgfmt.app.trergn.Overview; |
32 | import uk.me.parabola.mkgmap.Version; | |
23 | 33 | import uk.me.parabola.mkgmap.general.MapDataSource; |
24 | 34 | import uk.me.parabola.mkgmap.general.MapDetails; |
25 | 35 | import uk.me.parabola.mkgmap.general.MapLine; |
38 | 48 | protected final MapDetails mapper = new MapDetails(); |
39 | 49 | private EnhancedProperties configProps; |
40 | 50 | private boolean driveOnLeft; |
51 | private static final LocalDateTime now = LocalDateTime.now(); | |
41 | 52 | |
42 | 53 | /** |
43 | 54 | * Get the area that this map covers. Delegates to the map collector. |
131 | 142 | driveOnLeft = b; |
132 | 143 | } |
133 | 144 | |
145 | /** | |
146 | * Read the file given with the --copyright-file option | |
147 | * @param copyrightFileName the path to the file | |
148 | * @return copyright info stored in the file, with certain variable replacements | |
149 | */ | |
150 | public static String[] readCopyrightFile(String copyrightFileName ) { | |
151 | List<String> copyrightArray = new ArrayList<>(); | |
152 | try { | |
153 | File file = new File(copyrightFileName); | |
154 | copyrightArray = Files.readAllLines(file.toPath(), StandardCharsets.UTF_8); | |
155 | } | |
156 | catch (Exception e) { | |
157 | throw new ExitException("Error reading copyright file " + copyrightFileName, e); | |
158 | } | |
159 | if (!copyrightArray.isEmpty() && copyrightArray.get(0).startsWith("\ufeff")) | |
160 | copyrightArray.set(0, copyrightArray.get(0).substring(1)); | |
161 | UnaryOperator<String> replaceVariables = s -> s.replace("$MKGMAP_VERSION$", Version.VERSION) | |
162 | .replace("$JAVA_VERSION$", System.getProperty("java.version")) | |
163 | .replace("$YEAR$", Integer.toString(now.getYear())) | |
164 | .replace("$LONGDATE$", now.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG))) | |
165 | .replace("$SHORTDATE$", now.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT))) | |
166 | .replace("$TIME$", now.toLocalTime().toString().substring(0, 5)); | |
167 | copyrightArray.replaceAll(replaceVariables); | |
168 | String[] copyright = new String[copyrightArray.size()]; | |
169 | copyrightArray.toArray(copyright); | |
170 | return copyright; | |
171 | } | |
172 | ||
134 | 173 | } |
65 | 65 | |
66 | 66 | // Options |
67 | 67 | private final boolean ignoreTurnRestrictions; |
68 | private final String[] deadEndArgs; | |
68 | 69 | |
69 | 70 | /** name of the tag that contains a ;-separated list of tag names that should be removed after all elements have been processed */ |
70 | 71 | public static final short MKGMAP_REMOVE_TAG_KEY = TagDict.getInstance().xlate("mkgmap:removetags"); |
81 | 82 | } |
82 | 83 | |
83 | 84 | ignoreTurnRestrictions = args.getProperty("ignore-turn-restrictions", false) || !args.containsKey("route"); |
85 | deadEndArgs = args.getProperty("dead-ends", "fixme,FIXME").split(","); | |
84 | 86 | } |
85 | 87 | |
86 | 88 | /** |
219 | 221 | makeBoundaryNodes(); |
220 | 222 | |
221 | 223 | converter.setBoundingBox(getBoundingBox()); |
224 | converter.augmentWith(this); | |
225 | ||
222 | 226 | |
223 | 227 | for (Relation r : relationMap.values()) |
224 | 228 | converter.convertRelation(r); |
225 | 229 | |
226 | short fixmeTagKey = TagDict.getInstance().xlate("fixme"); | |
227 | short fixmeTagKey2 = TagDict.getInstance().xlate("FIXME"); | |
228 | for (Node n : nodeMap.values()){ | |
230 | for (Node n : nodeMap.values()) { | |
229 | 231 | converter.convertNode(n); |
230 | if (n.getTag(fixmeTagKey) != null || n.getTag(fixmeTagKey2) != null){ | |
231 | n.getLocation().setFixme(true); | |
232 | for (String deadEndArg : deadEndArgs) { | |
233 | String[] arg = deadEndArg.split("=", 2); | |
234 | String key = arg[0]; | |
235 | String value = arg.length < 2 || "*".equals(arg[1]) ? "" : arg[1]; | |
236 | String tagValue = n.getTag(key); | |
237 | if (tagValue != null && (tagValue.equals(value) || (value.isEmpty()))) { | |
238 | Coord location = n.getLocation(); | |
239 | if (location != null) | |
240 | location.setSkipDeadEndCheck(true); | |
241 | break; | |
242 | } | |
232 | 243 | } |
233 | 244 | } |
234 | 245 |
53 | 53 | */ |
54 | 54 | public void convertRelation(Relation relation); |
55 | 55 | |
56 | public default void augmentWith(uk.me.parabola.mkgmap.reader.osm.ElementSaver elementSaver) { | |
57 | } | |
58 | ||
56 | 59 | /** |
57 | 60 | * Set the bounding box for this map. This should be set before any other |
58 | 61 | * elements are converted if you want to use it. |
16 | 16 | package uk.me.parabola.mkgmap.reader.osm; |
17 | 17 | |
18 | 18 | import java.io.BufferedReader; |
19 | import java.io.File; | |
19 | import java.io.FileInputStream; | |
20 | 20 | import java.io.FileNotFoundException; |
21 | import java.io.InputStreamReader; | |
22 | import java.io.FileInputStream; | |
23 | 21 | import java.io.IOException; |
24 | 22 | import java.io.InputStream; |
23 | import java.io.InputStreamReader; | |
25 | 24 | import java.nio.charset.StandardCharsets; |
26 | import java.nio.file.Files; | |
27 | import java.time.LocalDateTime; | |
28 | import java.time.format.DateTimeFormatter; | |
29 | import java.time.format.FormatStyle; | |
30 | 25 | import java.util.ArrayList; |
31 | 26 | import java.util.HashMap; |
32 | 27 | import java.util.HashSet; |
33 | 28 | import java.util.List; |
34 | 29 | import java.util.Map; |
35 | 30 | import java.util.Set; |
36 | import java.util.function.UnaryOperator; | |
37 | ||
38 | import uk.me.parabola.imgfmt.ExitException; | |
31 | ||
39 | 32 | import uk.me.parabola.imgfmt.Utils; |
40 | 33 | import uk.me.parabola.log.Logger; |
41 | import uk.me.parabola.mkgmap.Version; | |
42 | 34 | import uk.me.parabola.mkgmap.general.LevelInfo; |
43 | 35 | import uk.me.parabola.mkgmap.general.LoadableMapDataSource; |
44 | 36 | import uk.me.parabola.mkgmap.osmstyle.NameFinder; |
79 | 71 | private final Set<String> usedTags = new HashSet<>(); |
80 | 72 | protected ElementSaver elementSaver; |
81 | 73 | protected OsmReadingHooks osmReadingHooks; |
82 | private static final LocalDateTime now = LocalDateTime.now(); | |
83 | 74 | |
84 | 75 | protected static final List<OsmHandler> handlers; |
85 | 76 | static { |
192 | 183 | public String[] copyrightMessages() { |
193 | 184 | String copyrightFileName = getConfig().getProperty("copyright-file", null); |
194 | 185 | if (copyrightFileName != null) { |
195 | List<String> copyrightArray = new ArrayList<>(); | |
196 | try { | |
197 | File file = new File(copyrightFileName); | |
198 | copyrightArray = Files.readAllLines(file.toPath(), StandardCharsets.UTF_8); | |
199 | } | |
200 | catch (Exception e) { | |
201 | throw new ExitException("Error reading copyright file " + copyrightFileName, e); | |
202 | } | |
203 | if (!copyrightArray.isEmpty() && copyrightArray.get(0).startsWith("\ufeff")) | |
204 | copyrightArray.set(0, copyrightArray.get(0).substring(1)); | |
205 | UnaryOperator<String> replaceVariables = s -> s.replace("$MKGMAP_VERSION$", Version.VERSION) | |
206 | .replace("$JAVA_VERSION$", System.getProperty("java.version")) | |
207 | .replace("$YEAR$", Integer.toString(now.getYear())) | |
208 | .replace("$LONGDATE$", now.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG))) | |
209 | .replace("$SHORTDATE$", now.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT))) | |
210 | .replace("$TIME$", now.toLocalTime().toString().substring(0, 5)); | |
211 | copyrightArray.replaceAll(replaceVariables); | |
212 | String[] copyright = new String[copyrightArray.size()]; | |
213 | copyrightArray.toArray(copyright); | |
214 | return copyright; | |
186 | return readCopyrightFile(copyrightFileName); | |
215 | 187 | } |
216 | 188 | String note = getConfig().getProperty("copyright-message", |
217 | 189 | "OpenStreetMap.org contributors. See: http://wiki.openstreetmap.org/index.php/Attribution"); |
264 | 236 | protected OsmReadingHooks pluginChain(ElementSaver saver, EnhancedProperties props) { |
265 | 237 | List<OsmReadingHooks> plugins = new ArrayList<>(); |
266 | 238 | for (OsmReadingHooks p : getPossibleHooks()) { |
239 | if (p instanceof ResidentialHook && style != null && !style.getUsedTags().contains("mkgmap:residential")) | |
240 | continue; | |
267 | 241 | if (p.init(saver, props)){ |
268 | 242 | plugins.add(p); |
269 | 243 | if (p instanceof RelationStyleHook) |
30 | 30 | * @param el The element as read from an OSM xml file in 'tag' format. |
31 | 31 | * @param result The resolved Garmin type that will go into the map. |
32 | 32 | */ |
33 | public void resolveType(Element el, TypeResult result); | |
33 | void resolveType(Element el, TypeResult result); | |
34 | 34 | |
35 | 35 | /** |
36 | 36 | * |
42 | 42 | * @param result The resolved Garmin type that will go into the map. |
43 | 43 | * @return |
44 | 44 | */ |
45 | public int resolveType(int cacheId, Element el, TypeResult result); | |
45 | int resolveType(int cacheId, Element el, TypeResult result); | |
46 | 46 | |
47 | 47 | /** |
48 | 48 | * Sets the finalize rules that are executed when |
50 | 50 | * |
51 | 51 | * @param finalizeRule finalize rule(s) |
52 | 52 | */ |
53 | public void setFinalizeRule(Rule finalizeRule); | |
53 | void setFinalizeRule(Rule finalizeRule); | |
54 | 54 | |
55 | public void printStats(String header); | |
55 | void printStats(String header); | |
56 | 56 | |
57 | public Rule getFinalizeRule(); | |
57 | Rule getFinalizeRule(); | |
58 | 58 | |
59 | public boolean containsExpression(String exp); | |
60 | ||
59 | boolean containsExpression(String exp); | |
60 | ||
61 | default void augmentWith(uk.me.parabola.mkgmap.reader.osm.ElementSaver elementSaver) {} | |
62 | ||
61 | 63 | } |
27 | 27 | import java.util.LinkedList; |
28 | 28 | import java.util.List; |
29 | 29 | import java.util.Map; |
30 | import java.util.Map.Entry; | |
31 | 30 | import java.util.NavigableMap; |
32 | 31 | import java.util.NavigableSet; |
33 | 32 | import java.util.Set; |
74 | 73 | private ElementSaver saver; |
75 | 74 | |
76 | 75 | private List<Way> shoreline = new ArrayList<>(); |
76 | private List<Way> islands = new ArrayList<>(); | |
77 | private List<Way> antiIslands = new ArrayList<>(); | |
78 | private Area tileBounds; | |
77 | 79 | private boolean generateSeaBackground = true; |
78 | 80 | |
79 | 81 | private String[] coastlineFilenames; |
425 | 427 | if (precompSea != null) |
426 | 428 | splitCoastLineToLineAndShape(way, natural); |
427 | 429 | else if (coastlineFilenames == null) { |
430 | /* RWB ??? | |
431 | * | |
432 | * I'd have thought it better to leave the original way, which has been saved, | |
433 | * untouched. The copy doesn't need any tags at this point. Later it might | |
434 | * be made into a polygon and tagged as land or sea. | |
435 | * | |
436 | * Could do a couple of quick check here to save effort later: | |
437 | * 1/ if no part in tile then stop, don't change anything or save. | |
438 | * 2/ if closed(), add to island list instead of shoreline. Any single closed | |
439 | * way will be a small island, not a sea! Later, after shoreline | |
440 | * has been merged/clipped etc, check these again for clipping and add clippings | |
441 | * to shoreline and unclipped back into islands | |
442 | */ | |
428 | 443 | // create copy of way that has only the natural=coastline tag |
429 | 444 | Way shore = new Way(way.getOriginalId(), way.getPoints()); |
430 | 445 | shore.setFakeId(); |
582 | 597 | } else { |
583 | 598 | // using polygons |
584 | 599 | // first add the complete bounding box as sea |
585 | saver.addWay(createSeaWay(saver.getBoundingBox(), false)); | |
600 | saver.addWay(createSeaWay(false)); | |
586 | 601 | } |
587 | 602 | |
588 | 603 | // check if the land tags need to be changed |
792 | 807 | */ |
793 | 808 | @Override |
794 | 809 | public void end() { |
810 | tileBounds = saver.getBoundingBox(); | |
795 | 811 | // precompiled sea has highest priority |
796 | 812 | // if it is set do not perform any other algorithm |
797 | 813 | if (precompSea != null && precompIndex.get() != null) { |
799 | 815 | return; |
800 | 816 | } |
801 | 817 | |
802 | final Area tileBounds = saver.getBoundingBox(); | |
803 | 818 | if (coastlineFilenames == null) { |
804 | 819 | log.info("Shorelines before join", shoreline.size()); |
805 | 820 | shoreline = joinWays(shoreline); |
815 | 830 | } |
816 | 831 | |
817 | 832 | // clip all shoreline segments |
818 | clipShorlineSegments(shoreline, tileBounds); | |
833 | clipShorlineSegments(); | |
819 | 834 | |
820 | 835 | if(shoreline.isEmpty()) { |
821 | 836 | // No sea required |
825 | 840 | // some sea |
826 | 841 | // No matter if the multipolygon option is used it is |
827 | 842 | // only necessary to create a land polygon |
828 | saver.addWay(createLandWay(tileBounds)); | |
843 | saver.addWay(createLandWay()); | |
829 | 844 | // nothing more to do |
830 | 845 | return; |
831 | 846 | } |
832 | 847 | |
833 | long multiId = FakeIdGenerator.makeFakeId(); | |
834 | 848 | Relation seaRelation = null; |
835 | if (generateSeaUsingMP) { | |
836 | log.debug("Generate seabounds relation", multiId); | |
837 | seaRelation = new GeneralRelation(multiId); | |
838 | seaRelation.addTag("type", "multipolygon"); | |
839 | seaRelation.addTag("natural", "sea"); | |
840 | } | |
841 | 849 | |
842 | ||
843 | List<Way> islands = new ArrayList<>(); | |
844 | ||
845 | 850 | // handle islands (closed shoreline components) first (they're easy) |
846 | handleIslands(shoreline, tileBounds, islands); | |
851 | handleIslands(); | |
852 | ||
853 | if (islands.isEmpty()) { | |
854 | // the tile doesn't contain any islands so we can assume | |
855 | // that it's showing a land mass that contains some, possibly | |
856 | // enclosed, sea areas - in which case, we don't want a sea | |
857 | // coloured background | |
858 | generateSeaBackground = false; | |
859 | } | |
847 | 860 | |
848 | 861 | // the remaining shoreline segments should intersect the boundary |
849 | 862 | // find the intersection points and store them in a SortedMap |
850 | NavigableMap<Double, Way> hitMap = findIntesectionPoints(shoreline, tileBounds, seaRelation); | |
851 | ||
852 | // now construct inner ways from these segments | |
853 | createLandPolygons(tileBounds, islands, hitMap); | |
854 | ||
855 | List<Way> antiIslands = removeAntiIslands(seaRelation, islands); | |
856 | if (islands.isEmpty()) { | |
857 | // the tile doesn't contain any islands so we can assume | |
858 | // that it's showing a land mass that contains some | |
859 | // enclosed sea areas - in which case, we don't want a sea | |
860 | // coloured background | |
861 | generateSeaBackground = false; | |
862 | } | |
863 | NavigableMap<Double, Way> hitMap = findIntesectionPoints(); | |
864 | verifyHits(hitMap); | |
863 | 865 | |
864 | 866 | if (generateSeaBackground) { |
865 | 867 | // the background is sea so all anti-islands should be |
866 | 868 | // contained by land otherwise they won't be visible |
867 | verifyIslands(islands, antiIslands, seaRelation); | |
868 | ||
869 | Way sea = createSeaWay(tileBounds, true); | |
869 | if (generateSeaUsingMP) { | |
870 | long multiId = FakeIdGenerator.makeFakeId(); | |
871 | log.debug("Generate seabounds relation", multiId); | |
872 | seaRelation = new GeneralRelation(multiId); | |
873 | seaRelation.addTag("type", "multipolygon"); | |
874 | seaRelation.addTag("natural", "sea"); | |
875 | } | |
876 | ||
877 | createLandPolygons(hitMap); | |
878 | processIslands(seaRelation); | |
879 | processAntiIslands(true); | |
880 | ||
881 | Way sea = createSeaWay(true); | |
870 | 882 | |
871 | 883 | log.info("sea: ", sea); |
872 | 884 | saver.addWay(sea); |
873 | if(seaRelation != null) { | |
885 | if(seaRelation != null) | |
874 | 886 | seaRelation.addElement("outer", sea); |
875 | } | |
876 | 887 | } else { |
877 | 888 | // background is land |
878 | ||
889 | createSeaPolygons(hitMap); | |
890 | processAntiIslands(false); | |
891 | ||
879 | 892 | // generate a land polygon so that the tile's |
880 | 893 | // background colour will match the land colour on the |
881 | 894 | // tiles that do contain some sea |
882 | Way land = createLandWay(tileBounds); | |
895 | Way land = createLandWay(); | |
883 | 896 | saver.addWay(land); |
884 | if(seaRelation != null) { | |
885 | seaRelation.addElement("inner", land); | |
886 | } | |
887 | } | |
888 | ||
889 | if (generateSeaUsingMP) { | |
897 | log.info("land:", land); | |
898 | } | |
899 | ||
900 | if (seaRelation != null) { | |
890 | 901 | SeaPolygonRelation coastRel = saver.createSeaPolyRelation(seaRelation); |
891 | 902 | coastRel.setFloodBlocker(floodblocker); |
892 | 903 | if (floodblocker) { |
901 | 912 | } |
902 | 913 | |
903 | 914 | shoreline = null; |
904 | } | |
905 | ||
906 | private void verifyIslands(List<Way> islands, List<Way> antiIslands, Relation seaRelation) { | |
915 | islands = null; | |
916 | antiIslands = null; | |
917 | } | |
918 | ||
919 | /** | |
920 | * These are bit of land that have been generated as polygons | |
921 | * @param seaRelation if set, add as inner | |
922 | */ | |
923 | private void processIslands(Relation seaRelation) { | |
924 | for (Way w : islands) { | |
925 | if (seaRelation != null) { | |
926 | // create a "inner" way for each island | |
927 | seaRelation.addElement("inner", w); | |
928 | } | |
929 | } | |
930 | } | |
931 | ||
932 | /** | |
933 | * These are bits of sea have been generated as polygons. | |
934 | * if the tile is also sea based, then check that surrounded by an island | |
935 | * @param seaRelation if set, add as inner | |
936 | * @param seaBased true if the tile is also sea with land [multi-]polygons | |
937 | */ | |
938 | private void processAntiIslands(boolean seaBased) { | |
907 | 939 | for (Way ai : antiIslands) { |
908 | boolean containedByLand = false; | |
909 | for (Way i : islands) { | |
910 | if (i.containsPointsOf(ai)) { | |
911 | containedByLand = true; | |
912 | break; | |
913 | } | |
914 | } | |
915 | ||
916 | if (!containedByLand) { | |
917 | // found an anti-island that is not contained by | |
918 | // land so convert it back into an island | |
919 | ai.deleteTag("natural"); | |
920 | ai.addTag(landTag[0], landTag[1]); | |
921 | if (seaRelation != null) { | |
922 | // create a "inner" way for the island | |
923 | seaRelation.addElement("inner", ai); | |
924 | } | |
925 | log.warn("Converting anti-island starting at", ai.getFirstPoint().toOSMURL(), | |
926 | "into an island as it is surrounded by water"); | |
927 | } | |
928 | } | |
929 | } | |
930 | ||
931 | private Way createLandWay(Area tileBounds) { | |
940 | if (seaBased) { | |
941 | boolean containedByLand = false; | |
942 | for (Way i : islands) { | |
943 | if (i.containsPointsOf(ai)) { | |
944 | containedByLand = true; | |
945 | break; | |
946 | } | |
947 | } | |
948 | if (!containedByLand) { | |
949 | // found an anti-island that is not contained by land | |
950 | log.warn("inner sea", ai , "is surrounded by water"); | |
951 | } | |
952 | } | |
953 | } | |
954 | } | |
955 | ||
956 | private Way createLandWay() { | |
932 | 957 | long landId = FakeIdGenerator.makeFakeId(); |
933 | 958 | Way land = new Way(landId, tileBounds.toCoords()); |
934 | 959 | land.addTag(landTag[0], landTag[1]); |
937 | 962 | |
938 | 963 | /** |
939 | 964 | * Create a sea polygon from the given tile bounds |
940 | * @param tileBounds | |
941 | 965 | * @param enlarge if true, make sure that the polygon is slightly larger than the tile bounds |
942 | 966 | * @return the created way |
943 | 967 | */ |
944 | private static Way createSeaWay(Area tileBounds, boolean enlarge) { | |
968 | private Way createSeaWay(boolean enlarge) { | |
945 | 969 | log.info("generating sea, seaBounds=", tileBounds); |
946 | 970 | Area bbox = tileBounds; |
947 | 971 | long seaId = FakeIdGenerator.makeFakeId(); |
954 | 978 | bbox = new Area(bbox.getMinLat() - 1, bbox.getMinLong() - 1, bbox.getMaxLat() + 1, bbox.getMaxLong() + 1); |
955 | 979 | } |
956 | 980 | Way sea = new Way(seaId, bbox.toCoords()); |
981 | sea.reverse(); // make clockwise for consistency | |
957 | 982 | sea.addTag("natural", "sea"); |
958 | 983 | sea.setFullArea(SEA_SIZE); |
959 | 984 | return sea; |
964 | 989 | * @param shoreline All the the ways making up the coast. |
965 | 990 | * @param bounds The map bounds. |
966 | 991 | */ |
967 | private static void clipShorlineSegments(List<Way> shoreline, Area bounds) { | |
992 | private void clipShorlineSegments() { | |
968 | 993 | List<Way> toBeRemoved = new ArrayList<>(); |
969 | 994 | List<Way> toBeAdded = new ArrayList<>(); |
970 | 995 | for (Way segment : shoreline) { |
971 | 996 | List<Coord> points = segment.getPoints(); |
972 | List<List<Coord>> clipped = LineClipper.clip(bounds, points); | |
997 | List<List<Coord>> clipped = LineClipper.clip(tileBounds, points); | |
973 | 998 | if (clipped != null) { |
974 | 999 | log.info("clipping", segment); |
975 | 1000 | toBeRemoved.add(segment); |
987 | 1012 | } |
988 | 1013 | |
989 | 1014 | /** |
990 | * Pick out the islands and save them for later. They are removed from the | |
991 | * shore line list and added to the island list. | |
992 | * | |
993 | * @param shoreline The collected shore line ways. | |
994 | * @param tileBounds The map boundary. | |
995 | * @param islands The islands are saved to this list. | |
996 | */ | |
997 | private void handleIslands(List<Way> shoreline, Area tileBounds, List<Way> islands) { | |
1015 | * Pick out the closed ways and save them for later. They are removed from the | |
1016 | * shore line list and added to the [anti]island list. | |
1017 | */ | |
1018 | private void handleIslands() { | |
998 | 1019 | Iterator<Way> it = shoreline.iterator(); |
999 | 1020 | while (it.hasNext()) { |
1000 | 1021 | Way w = it.next(); |
1001 | 1022 | if (w.hasIdenticalEndPoints()) { |
1002 | log.info("adding island", w); | |
1003 | islands.add(w); | |
1023 | addClosedShore(w); | |
1004 | 1024 | it.remove(); |
1005 | 1025 | } |
1006 | 1026 | } |
1007 | 1027 | |
1008 | closeGaps(shoreline, tileBounds); | |
1028 | closeGaps(); | |
1009 | 1029 | // there may be more islands now |
1010 | 1030 | it = shoreline.iterator(); |
1011 | 1031 | while (it.hasNext()) { |
1012 | 1032 | Way w = it.next(); |
1013 | 1033 | if (w.hasIdenticalEndPoints()) { |
1014 | log.debug("island after concatenating"); | |
1015 | islands.add(w); | |
1034 | log.debug("closed after concatenating", w); | |
1035 | addClosedShore(w); | |
1016 | 1036 | it.remove(); |
1017 | 1037 | } |
1018 | 1038 | } |
1019 | 1039 | } |
1020 | 1040 | |
1021 | private void closeGaps(List<Way> shoreline, Area bounds) { | |
1041 | private void closeGaps() { | |
1022 | 1042 | if (maxCoastlineGap <= 0) |
1023 | 1043 | return; |
1024 | 1044 | |
1033 | 1053 | if (w1.hasIdenticalEndPoints()) |
1034 | 1054 | continue; |
1035 | 1055 | Coord w1e = w1.getLastPoint(); |
1036 | if (!bounds.onBoundary(w1e)) { | |
1037 | Way closed = tryCloseGap(shoreline, w1, bounds); | |
1056 | if (!tileBounds.onBoundary(w1e)) { | |
1057 | Way closed = tryCloseGap(w1); | |
1038 | 1058 | if (closed != null) { |
1039 | 1059 | saver.addWay(closed); |
1040 | 1060 | changed = true; |
1044 | 1064 | } while (changed); |
1045 | 1065 | } |
1046 | 1066 | |
1047 | private Way tryCloseGap(List<Way> shoreline, Way w1, Area bounds) { | |
1067 | private Way tryCloseGap(Way w1) { | |
1048 | 1068 | Coord w1e = w1.getLastPoint(); |
1049 | 1069 | Way nearest = null; |
1050 | 1070 | double smallestGap = Double.MAX_VALUE; |
1052 | 1072 | if (w1 == w2 || w2.hasIdenticalEndPoints()) |
1053 | 1073 | continue; |
1054 | 1074 | Coord w2s = w2.getFirstPoint(); |
1055 | if (!bounds.onBoundary(w2s)) { | |
1075 | if (!tileBounds.onBoundary(w2s)) { | |
1056 | 1076 | double gap = w1e.distance(w2s); |
1057 | 1077 | if (gap < smallestGap) { |
1058 | 1078 | nearest = w2; |
1087 | 1107 | return null; |
1088 | 1108 | } |
1089 | 1109 | |
1110 | private void addClosedShore(Way w) { | |
1111 | if (Way.clockwise(w.getPoints())) | |
1112 | addAsSea(w); | |
1113 | else | |
1114 | addAsLand(w); | |
1115 | } | |
1116 | ||
1117 | private void addAsSea(Way w) { | |
1118 | w.addTag("natural", "sea"); | |
1119 | log.info("adding anti-island", w); | |
1120 | antiIslands.add(w); | |
1121 | w.setFullArea(SEA_SIZE); | |
1122 | saver.addWay(w); | |
1123 | } | |
1124 | ||
1125 | private void addAsLand(Way w) { | |
1126 | w.addTag(landTag[0], landTag[1]); | |
1127 | log.info("adding island", w); | |
1128 | islands.add(w); | |
1129 | saver.addWay(w); | |
1130 | } | |
1131 | ||
1090 | 1132 | /** |
1091 | 1133 | * Add lines to ways that touch or cross the sea bounds so that the way is closed along the edges of the bounds. |
1092 | 1134 | * Adds complete edges or parts of them. This is done counter-clockwise. |
1093 | * @param tileBounds the bounds | |
1094 | * @param islands list of land masses to which the closed ways are added | |
1095 | 1135 | * @param hitMap A map of the 'hits' where the shore line intersects the boundary. |
1096 | 1136 | */ |
1097 | private void createLandPolygons(Area tileBounds, List<Way> islands, NavigableMap<Double, Way> hitMap) { | |
1137 | private void createLandPolygons(NavigableMap<Double, Way> hitMap) { | |
1098 | 1138 | NavigableSet<Double> hits = hitMap.navigableKeySet(); |
1099 | 1139 | while (!hits.isEmpty()) { |
1100 | 1140 | Way w = new Way(FakeIdGenerator.makeFakeId()); |
1101 | saver.addWay(w); | |
1102 | ||
1103 | Double hit = hits.first(); | |
1104 | Double hFirst = hit; | |
1141 | Double hFirst = hits.first(); | |
1142 | Double hStart = hFirst, hEnd; | |
1143 | boolean finished = false; | |
1105 | 1144 | do { |
1106 | Way segment = hitMap.get(hit); | |
1107 | log.info("current hit:", hit); | |
1108 | Double hNext; | |
1109 | if (segment != null) { | |
1110 | // add the segment and get the "ending hit" | |
1111 | log.info("adding:", segment); | |
1112 | segment.getPoints().forEach(w::addPointIfNotEqualToLastPoint); | |
1113 | ||
1114 | hNext = getEdgeHit(tileBounds, segment.getLastPoint()); | |
1115 | } else { | |
1116 | w.addPointIfNotEqualToLastPoint(getPoint(tileBounds, hit)); | |
1117 | hNext = hits.higher(hit); | |
1118 | if (hNext == null) | |
1119 | hNext = hFirst; | |
1120 | addCorners(w, tileBounds, hit, hNext); | |
1121 | ||
1122 | } | |
1123 | hits.remove(hit); | |
1124 | hit = hNext; | |
1125 | } while (!hits.isEmpty() && Double.compare(hFirst, hit) != 0); | |
1126 | ||
1127 | if (!w.hasIdenticalEndPoints()) { | |
1128 | if (w.getFirstPoint().highPrecEquals(w.getLastPoint())) { | |
1129 | w.getPoints().remove(w.getPoints().size() - 1); | |
1130 | } | |
1131 | w.addPoint(w.getFirstPoint()); // close shape | |
1132 | } | |
1133 | log.info("adding non-island landmass, hits.size()=" + hits.size()); | |
1134 | islands.add(w); | |
1135 | } | |
1136 | } | |
1137 | ||
1138 | private static void addCorners(Way w, Area tileBounds, double hit, double hNext) { | |
1139 | if (hit != hNext) { | |
1140 | int startEdge = (int) hit; | |
1141 | int endEdge = (int) hNext; | |
1142 | if (endEdge < startEdge) | |
1143 | endEdge += 4; | |
1144 | log.info("joining: ", hit, hNext); | |
1145 | for (int i = startEdge; i < endEdge; i++) { | |
1146 | int edge = i < 4 ? i : i - 4; | |
1147 | Coord p = getPoint(tileBounds, edge + 1.0); | |
1148 | w.addPointIfNotEqualToLastPoint(p); | |
1149 | } | |
1150 | } | |
1151 | w.addPointIfNotEqualToLastPoint(getPoint(tileBounds, hNext)); | |
1152 | } | |
1153 | ||
1154 | /** | |
1155 | * An 'anti-island' is something that has been detected as an island, but the water | |
1156 | * is on the inside. I think you would call this a lake. | |
1157 | * @param seaRelation The relation holding the sea. Only set if we are using multi-polygons for | |
1158 | * the sea. | |
1159 | * @param islands The island list that was found earlier. | |
1160 | * @return The so-called anti-islands. | |
1161 | */ | |
1162 | private List<Way> removeAntiIslands(Relation seaRelation, List<Way> islands) { | |
1163 | List<Way> antiIslands = new ArrayList<>(); | |
1164 | Iterator<Way> iter = islands.iterator(); | |
1165 | while (iter.hasNext()) { | |
1166 | Way w = iter.next(); | |
1167 | if (!FakeIdGenerator.isFakeId(w.getId())) { | |
1168 | w = copyWithNameTags(w); | |
1169 | } | |
1170 | ||
1171 | // determine where the water is | |
1172 | if (Way.clockwise(w.getPoints())) { | |
1173 | // water on the inside of the poly, it's an | |
1174 | // "anti-island" so tag with natural=water (to | |
1175 | // make it visible above the land) | |
1176 | w.addTag("natural", "water"); | |
1177 | antiIslands.add(w); | |
1178 | iter.remove(); | |
1179 | saver.addWay(w); | |
1180 | } else { | |
1181 | // water on the outside of the poly, it's an island | |
1182 | w.addTag(landTag[0], landTag[1]); | |
1183 | saver.addWay(w); | |
1184 | if (seaRelation != null) { | |
1185 | // create a "inner" way for each island | |
1186 | seaRelation.addElement("inner", w); | |
1187 | } | |
1188 | } | |
1189 | } | |
1190 | return antiIslands; | |
1191 | } | |
1192 | ||
1193 | /** | |
1194 | * Create copy of way, but ignore tags that don't contain "name" in the key | |
1195 | * @param w | |
1196 | * @return | |
1197 | */ | |
1198 | private static Way copyWithNameTags(Way w) { | |
1199 | Way w1 = new Way(w.getOriginalId(), w.getPoints()); | |
1200 | w1.setFakeId(); | |
1201 | for (Entry<String, String> tagEntry : w.getTagEntryIterator()) { | |
1202 | if ("name".equals(tagEntry.getKey()) || tagEntry.getKey().contains("name")) { | |
1203 | w1.addTag(tagEntry.getKey(), tagEntry.getValue()); | |
1204 | } | |
1205 | } | |
1206 | return w1; | |
1145 | Way segment = hitMap.get(hStart); | |
1146 | log.info("current hit:", hStart, "adding:", segment); | |
1147 | segment.getPoints().forEach(w::addPointIfNotEqualToLastPoint); | |
1148 | hits.remove(hStart); | |
1149 | hEnd = getEdgeHit(tileBounds, segment.getLastPoint()); | |
1150 | if (hEnd < hStart) // gone all the way around | |
1151 | finished = true; | |
1152 | else { // if another, join it on | |
1153 | hStart = hits.higher(hEnd); | |
1154 | if (hStart == null) { | |
1155 | hFirst += 4; | |
1156 | finished = true; | |
1157 | } | |
1158 | } | |
1159 | if (finished) | |
1160 | hStart = hFirst; | |
1161 | addCorners(w, hEnd, hStart); | |
1162 | } while (!finished); | |
1163 | w.addPoint(w.getFirstPoint()); // close shape | |
1164 | log.info("adding landPoly, hits.size()", hits.size()); | |
1165 | addAsLand(w); | |
1166 | } | |
1167 | } | |
1168 | ||
1169 | /** | |
1170 | * Add lines to ways that touch or cross the sea bounds so that the way is closed along the edges of the bounds. | |
1171 | * Adds complete edges or parts of them. This is done clockwise. | |
1172 | * This is much the same as createLandPolygons, but in reverse. | |
1173 | * @param hitMap A map of the 'hits' where the shore line intersects the boundary. | |
1174 | */ | |
1175 | private void createSeaPolygons(NavigableMap<Double, Way> hitMap) { | |
1176 | NavigableSet<Double> hits = hitMap.navigableKeySet(); | |
1177 | while (!hits.isEmpty()) { | |
1178 | Way w = new Way(FakeIdGenerator.makeFakeId()); | |
1179 | Double hFirst = hits.last(); | |
1180 | Double hStart = hFirst, hEnd; | |
1181 | boolean finished = false; | |
1182 | do { | |
1183 | Way segment = hitMap.get(hStart); | |
1184 | log.info("current hit:", hStart, "adding:", segment); | |
1185 | segment.getPoints().forEach(w::addPointIfNotEqualToLastPoint); | |
1186 | hits.remove(hStart); | |
1187 | hEnd = getEdgeHit(tileBounds, segment.getLastPoint()); | |
1188 | if (hEnd > hStart) // gone all the way around | |
1189 | finished = true; | |
1190 | else { // if another, join it on | |
1191 | hStart = hits.lower(hEnd); | |
1192 | if (hStart == null) { | |
1193 | hEnd += 4; | |
1194 | finished = true; | |
1195 | } | |
1196 | } | |
1197 | if (finished) | |
1198 | hStart = hFirst; | |
1199 | addCorners(w, hEnd, hStart); | |
1200 | } while (!finished); | |
1201 | w.addPoint(w.getFirstPoint()); // close shape | |
1202 | log.info("adding seaPoly, hits.size()", hits.size()); | |
1203 | addAsSea(w); | |
1204 | } | |
1205 | } | |
1206 | ||
1207 | /** | |
1208 | * Append corner points to the way if necessary, to give lines along the edges of the bounds | |
1209 | * It is possible that the line needs to go all the way around the tile! | |
1210 | * The relationship between hFrom and hTo determines the direction | |
1211 | * @param w the way | |
1212 | * @param hFrom going from this edgeHit (0 >= hit < 8) | |
1213 | * @param hTo to this edgeHit (ditto) | |
1214 | */ | |
1215 | private void addCorners(Way w, double hFrom, double hTo) { | |
1216 | int startEdge = (int)hFrom; | |
1217 | int endEdge = (int)hTo; | |
1218 | int direction, toCorner; | |
1219 | if (hFrom < hTo) { // increasing, anti-clockwise, land | |
1220 | direction = +1; | |
1221 | toCorner = 1; | |
1222 | } else { // decreasing, clockwise, sea | |
1223 | direction = -1; | |
1224 | toCorner = 0; // (int)hFrom does the -1 | |
1225 | } | |
1226 | log.debug("addCorners", hFrom, hTo, direction, startEdge, endEdge, toCorner); | |
1227 | while (startEdge != endEdge) { | |
1228 | Coord p = getPoint(tileBounds, startEdge + toCorner); | |
1229 | w.addPointIfNotEqualToLastPoint(p); | |
1230 | startEdge += direction; | |
1231 | } | |
1207 | 1232 | } |
1208 | 1233 | |
1209 | 1234 | /** |
1210 | 1235 | * Find the points where the remaining shore line segments intersect with the |
1211 | 1236 | * map boundary. |
1212 | * | |
1213 | * @param shoreline The remaining shore line segments. | |
1214 | * @param tileBounds The map boundary. | |
1215 | * @param seaRelation If we are using a multi-polygon, this is it. Otherwise it will be null. | |
1216 | 1237 | * @return A map of the 'hits' where the shore line intersects the boundary. |
1217 | 1238 | */ |
1218 | private NavigableMap<Double, Way> findIntesectionPoints(List<Way> shoreline, Area tileBounds, Relation seaRelation) { | |
1219 | if (generateSeaUsingMP && seaRelation == null) | |
1220 | throw new MapFailedException("seaRelation is null"); | |
1221 | ||
1239 | private NavigableMap<Double, Way> findIntesectionPoints() { | |
1222 | 1240 | NavigableMap<Double, Way> hitMap = new TreeMap<>(); |
1223 | 1241 | for (Way w : shoreline) { |
1224 | 1242 | Coord pStart = w.getFirstPoint(); |
1230 | 1248 | // nice case: both ends touch the boundary |
1231 | 1249 | log.debug("hits: ", hStart, hEnd); |
1232 | 1250 | hitMap.put(hStart, w); |
1233 | hitMap.put(hEnd, null); | |
1251 | hitMap.put(hEnd, null); // put this for verifyHits which then deletes it | |
1234 | 1252 | } else { |
1235 | /* | |
1253 | /* | |
1236 | 1254 | * This problem occurs usually when the shoreline is cut by osmosis (e.g. country-extracts from geofabrik) |
1237 | * There are two possibilities to solve this problem: | |
1238 | * 1. Close the way and treat it as an island. This is sometimes the best solution (Germany: Usedom at the | |
1239 | * border to Poland) | |
1240 | * 2. Create a "sea sector" only for this shoreline segment. This may also be the best solution | |
1241 | * (see German border to the Netherlands where the shoreline continues in the Netherlands) | |
1242 | * The first choice may lead to "flooded" areas, the second may lead to "triangles". | |
1243 | * | |
1244 | * Usually, the first choice is appropriate if the segment is "nearly" closed. | |
1255 | * and so a tile, covering land outside the selected area, has bits of unclosed shoreline that | |
1256 | * don't start and finish outside the tile. | |
1257 | * There are various possibilities to show a reasonable map, but there is no full solution. | |
1258 | * Mkmap offers various options: | |
1259 | * 1. Use --precomp-sea=... This has all the coastline and the following is N/A. | |
1260 | * 2. Close short gaps in the coastline; eg --generate-sea=...,close-gaps=500 | |
1261 | * Harbour mouths are often fixed by this. | |
1262 | * 3. Create a "sea sector" for this shoreline segment. This is a right-angle triangle where the | |
1263 | * the hypotenuse is the shoreline. "sea sector" is a slight mis-nomer because, if the | |
1264 | * tile is sea-based, a "land sector" is created. Often this will show the coast in | |
1265 | * a meaningful way, but it can create a self-intersecting polygons and, if other bits of | |
1266 | * shoreline that reach the edge of the tile cause this area to be the same type, it won't show | |
1267 | * 4. Extend the ends of the shoreline to the nearest edge of the tile with ...,extend-sea-sectors | |
1268 | * This, in conjunction with close-gaps, normally works well but it isn't foolproof. | |
1245 | 1269 | */ |
1246 | 1270 | List<Coord> points = w.getPoints(); |
1247 | boolean nearlyClosed = pStart.distance(pEnd) < 0.1 * w.calcLengthInMetres(); | |
1248 | ||
1249 | if (nearlyClosed) { | |
1250 | // close the way | |
1251 | points.add(pStart); // XXX original way is modified, is that correct? | |
1252 | ||
1253 | if (!FakeIdGenerator.isFakeId(w.getId())) { | |
1254 | w = copyWithNameTags(w); | |
1271 | if (allowSeaSectors) { | |
1272 | Way seaOrLand = new Way(FakeIdGenerator.makeFakeId()); | |
1273 | seaOrLand.getPoints().addAll(points); | |
1274 | int startLat = pStart.getHighPrecLat(); | |
1275 | int startLon = pStart.getHighPrecLon(); | |
1276 | int endLat = pEnd.getHighPrecLat(); | |
1277 | int endLon = pEnd.getHighPrecLon(); | |
1278 | boolean startLatIsCorner = (startLat > endLat) == (startLon > endLon); | |
1279 | int cornerLat, cornerLon; | |
1280 | if (generateSeaBackground) { // the tile is sea, with islands | |
1281 | startLatIsCorner = !startLatIsCorner; | |
1282 | addAsLand(seaOrLand); | |
1283 | } else { // the tile is land, maybe with sea polygons on edge | |
1284 | addAsSea(seaOrLand); | |
1255 | 1285 | } |
1256 | w.addTag(landTag[0], landTag[1]); | |
1257 | saver.addWay(w); | |
1258 | if (generateSeaUsingMP) { | |
1259 | seaRelation.addElement("inner", w); | |
1286 | if (startLatIsCorner) { | |
1287 | cornerLat = startLat; | |
1288 | cornerLon = endLon; | |
1289 | } else { | |
1290 | cornerLat = endLat; | |
1291 | cornerLon = startLon; | |
1260 | 1292 | } |
1261 | } else if(allowSeaSectors) { | |
1262 | Way sea; | |
1263 | if (generateSeaUsingMP) { | |
1264 | sea = new Way(seaRelation.getOriginalId()); | |
1265 | sea.setFakeId(); | |
1266 | } else { | |
1267 | sea = new Way(FakeIdGenerator.makeFakeId()); | |
1268 | } | |
1269 | sea.getPoints().addAll(points); | |
1270 | sea.addPoint(new Coord(pEnd.getLatitude(), pStart.getLongitude())); | |
1271 | sea.addPoint(pStart); | |
1272 | sea.addTag("natural", "sea"); | |
1273 | log.info("sea: ", sea); | |
1274 | saver.addWay(sea); | |
1275 | if(generateSeaUsingMP) | |
1276 | seaRelation.addElement("outer", sea); | |
1277 | generateSeaBackground = false; | |
1293 | seaOrLand.addPoint(Coord.makeHighPrecCoord(cornerLat, cornerLon)); | |
1294 | seaOrLand.addPoint(pStart); | |
1295 | log.info("seaSector: ", generateSeaBackground, startLatIsCorner, Way.clockwise(seaOrLand.getPoints()), seaOrLand); | |
1278 | 1296 | } else if (extendSeaSectors) { |
1279 | // create additional points at next border to prevent triangles from point 2 | |
1297 | // join to nearest tile border | |
1280 | 1298 | if (null == hStart) { |
1281 | 1299 | hStart = getNextEdgeHit(tileBounds, pStart); |
1282 | 1300 | w.getPoints().add(0, getPoint(tileBounds, hStart)); |
1287 | 1305 | } |
1288 | 1306 | log.debug("hits (second try): ", hStart, hEnd); |
1289 | 1307 | hitMap.put(hStart, w); |
1290 | hitMap.put(hEnd, null); | |
1308 | hitMap.put(hEnd, null); // put this for verifyHits which then deletes it | |
1291 | 1309 | } else { |
1292 | 1310 | // show the coastline even though we can't produce |
1293 | 1311 | // a polygon for the land |
1294 | 1312 | w.addTag("natural", "coastline"); |
1295 | if (!w.hasIdenticalEndPoints()) { | |
1296 | log.error("adding sea shape that is not really closed"); | |
1297 | } | |
1313 | log.error("adding sea shape that is not really closed"); | |
1298 | 1314 | saver.addWay(w); |
1299 | 1315 | } |
1300 | 1316 | } |
1301 | 1317 | } |
1302 | 1318 | return hitMap; |
1319 | } | |
1320 | ||
1321 | /* | |
1322 | * Check the hitHap has alternating start & end of ways - adjacent coastlines on the tile | |
1323 | * boundary must be in opposite directions. There may be other errors, for instance crossing (twice) | |
1324 | * due to extendSeaSectors when there is another bit of coastline in the gap, that this doesn't detect. | |
1325 | * After checking, the end hit is removed | |
1326 | */ | |
1327 | private void verifyHits(NavigableMap<Double, Way> hitMap) { | |
1328 | log.debug("Islands", islands.size(), "Seas", antiIslands.size(), "hits", hitMap.size()); | |
1329 | NavigableSet<Double> hits = hitMap.navigableKeySet(); | |
1330 | Iterator<Double> iter = hits.iterator(); | |
1331 | int lastStatus = 0, thisStatus; | |
1332 | while (iter.hasNext()) { | |
1333 | Double aHit = iter.next(); | |
1334 | Way segment = hitMap.get(aHit); | |
1335 | log.debug("hitmap", aHit, segment); | |
1336 | if (segment == null) { | |
1337 | thisStatus = -1; | |
1338 | iter.remove(); | |
1339 | } else { | |
1340 | thisStatus = +1; | |
1341 | } | |
1342 | if (thisStatus == lastStatus) | |
1343 | log.error("Adjacent coastlines hit tile edge in same direction", aHit, segment); | |
1344 | lastStatus = thisStatus; | |
1345 | } | |
1303 | 1346 | } |
1304 | 1347 | |
1305 | 1348 | // create the point where the shoreline hits the sea bounds |
1333 | 1376 | plonHp = aMinLongHP; |
1334 | 1377 | break; |
1335 | 1378 | default: |
1336 | throw new MapFailedException("illegal state"); | |
1379 | throw new MapFailedException("GetPoint edge: " + edgePos); | |
1337 | 1380 | } |
1338 | 1381 | return Coord.makeHighPrecCoord(platHp, plonHp); |
1339 | 1382 | } |
162 | 162 | * The returned Tags must not be modified by the caller. |
163 | 163 | */ |
164 | 164 | public Tags get(Coord co){ |
165 | Tags res = root.get(co/*, "_"*/); | |
166 | if (res == null && bbox.contains(co.getLongitude(),co.getLatitude())){ | |
167 | // we did not find the point, probably it lies on a boundary and | |
168 | // the clauses regarding insideness of areas make it "invisible" | |
169 | // try again a few other nearby points | |
170 | Coord neighbour1 = new Coord(co.getLatitude()-1, co.getLongitude()); | |
171 | Coord neighbour2 = new Coord(co.getLatitude() , co.getLongitude()-1); | |
172 | Coord neighbour3 = new Coord(co.getLatitude()+1, co.getLongitude()); | |
173 | Coord neighbour4 = new Coord(co.getLatitude() , co.getLongitude()+1); | |
174 | res = root.get(neighbour1/*, "_"*/); | |
175 | if (res == null) | |
176 | res = root.get(neighbour2/*, "_"*/); | |
177 | if (res == null) | |
178 | res = root.get(neighbour3/*, "_"*/); | |
179 | if (res == null) | |
180 | res = root.get(neighbour4/*, "_"*/); | |
165 | return get(co, true); | |
166 | } | |
167 | ||
168 | /** | |
169 | * Return location relevant Tags for the point defined by Coord | |
170 | * @param co the point | |
171 | * @param tryAlsoNearby if true, try also nearby points | |
172 | * @return a reference to the internal Tags or null if the point was not found. | |
173 | * The returned Tags must not be modified by the caller. | |
174 | */ | |
175 | public Tags get(Coord co, boolean tryAlsoNearby) { | |
176 | Tags res = root.get(co); | |
177 | if (res == null && tryAlsoNearby) { | |
178 | int lonHp = co.getHighPrecLon(); | |
179 | int latHp = co.getHighPrecLat(); | |
180 | double x = (double) lonHp / (1 << Coord.DELTA_SHIFT); | |
181 | double y = (double) latHp / (1 << Coord.DELTA_SHIFT); | |
182 | int radius = 1 << Coord.DELTA_SHIFT; | |
183 | if ( bbox.contains(x, y)) { | |
184 | // try again a few other nearby points | |
185 | res = root.get(Coord.makeHighPrecCoord(latHp + radius, lonHp)); | |
186 | if (res == null) | |
187 | res = root.get(Coord.makeHighPrecCoord(latHp, lonHp + radius)); | |
188 | if (res == null) | |
189 | res = root.get(Coord.makeHighPrecCoord(latHp - radius, lonHp)); | |
190 | if (res == null) | |
191 | res = root.get(Coord.makeHighPrecCoord(latHp, lonHp - radius)); | |
192 | } | |
181 | 193 | } |
182 | 194 | return res; |
183 | 195 | } |
238 | 250 | return root.getCoveredArea(admLevel, "_"); |
239 | 251 | } |
240 | 252 | |
241 | /** | |
242 | * Return boundary names relevant for the point defined by Coord | |
243 | * @param co the point | |
244 | * @return A string with a boundary Id, optionally followed by pairs of admlevel:boundary Id. | |
245 | * Sample: r1184826;6:r62579;4:r62372;2:r51477 | |
246 | */ | |
247 | public String getBoundaryNames(Coord co){ | |
248 | return root.getBoundaryNames(co); | |
249 | } | |
250 | ||
251 | ||
252 | 253 | /** |
253 | 254 | * Save the BoundaryQuadTree to an open stream. The format is QUADTREE_DATA_FORMAT. |
254 | 255 | * @param stream |
325 | 326 | try { |
326 | 327 | while (true) { |
327 | 328 | String type = inpStream.readUTF(); |
328 | if (type.equals("TAGS")){ | |
329 | if ("TAGS".equals(type)){ | |
329 | 330 | String id = inpStream.readUTF(); |
330 | 331 | Tags tags = new Tags(); |
331 | 332 | int noOfTags = inpStream.readInt(); |
336 | 337 | } |
337 | 338 | boundaryTags.put(id, tags); |
338 | 339 | } |
339 | else if (type.equals("AREA")){ | |
340 | else if ("AREA".equals(type)){ | |
340 | 341 | if (isFirstArea){ |
341 | 342 | isFirstArea = false; |
342 | 343 | prepareLocationInfo(); |
457 | 458 | } |
458 | 459 | |
459 | 460 | /** |
460 | * Return boundary names relevant for the point defined by Coord | |
461 | * @param co the point | |
462 | * @return A string with a boundary Id, optionally followed by pairs of admlevel:boundary Id. | |
463 | * Sample: r1184826;6:r62579;4:r62372;2:r51477 | |
464 | */ | |
465 | private String getBoundaryNames(Coord co) { | |
466 | if (!this.bounds.contains(co)) | |
467 | return null; | |
468 | if (isLeaf){ | |
469 | if (nodes == null || nodes.isEmpty()) | |
470 | return null; | |
471 | int lon = co.getLongitude(); | |
472 | int lat = co.getLatitude(); | |
473 | for (NodeElem nodeElem : nodes) { | |
474 | if (nodeElem.tagMask > 0 && nodeElem.getArea().contains(lon, lat)) { | |
475 | if (nodeElem.locationDataSrc != null) | |
476 | return nodeElem.boundaryId + ";" + nodeElem.locationDataSrc; | |
477 | return nodeElem.boundaryId; | |
478 | } | |
479 | } | |
480 | } | |
481 | else { | |
482 | for (int i = 0; i < 4; i++){ | |
483 | String res = childs[i].getBoundaryNames(co); | |
484 | if (res != null) | |
485 | return res; | |
486 | } | |
487 | } | |
488 | return null; | |
489 | } | |
490 | ||
491 | /** | |
492 | 461 | * Return location relevant Tags for the point defined by Coord |
493 | 462 | * @param co the point |
494 | 463 | * @return a reference to the internal Tags or null if the point was not found. |
497 | 466 | private Tags get(Coord co/*, String treePath*/){ |
498 | 467 | if (!this.bounds.contains(co)) |
499 | 468 | return null; |
500 | if (isLeaf){ | |
469 | if (isLeaf) { | |
501 | 470 | if (nodes == null || nodes.isEmpty()) |
502 | 471 | return null; |
503 | int lon = co.getLongitude(); | |
504 | int lat = co.getLatitude(); | |
472 | double lon = (double) co.getHighPrecLon() / (1 << Coord.DELTA_SHIFT); | |
473 | double lat = (double) co.getHighPrecLat() / (1 << Coord.DELTA_SHIFT); | |
505 | 474 | for (NodeElem nodeElem : nodes) { |
506 | 475 | if (nodeElem.tagMask > 0 && nodeElem.getArea().contains(lon, lat)) { |
507 | 476 | return nodeElem.locTags; |
508 | 477 | } |
509 | 478 | } |
510 | } | |
511 | else { | |
512 | for (int i = 0; i < 4; i++){ | |
513 | Tags res = childs[i].get(co/*, treePath+i*/); | |
514 | if (res != null) | |
515 | return res; | |
479 | } else { | |
480 | for (int i = 0; i < 4; i++) { | |
481 | Tags res = childs[i].get(co/* , treePath+i */); | |
482 | if (res != null) | |
483 | return res; | |
516 | 484 | } |
517 | 485 | } |
518 | 486 | return null; |
754 | 722 | } |
755 | 723 | long t1 = System.currentTimeMillis(); |
756 | 724 | if (DEBUG){ |
757 | if (treePath.equals(DEBUG_TREEPATH) || DEBUG_TREEPATH.equals("all")){ | |
725 | if (treePath.equals(DEBUG_TREEPATH) || "all".equals(DEBUG_TREEPATH)) { | |
758 | 726 | for (NodeElem nodeElem: nodes){ |
759 | 727 | nodeElem.saveGPX("start",treePath); |
760 | 728 | } |
772 | 740 | for (int i=0; i < nodes.size(); i++){ |
773 | 741 | NodeElem toAdd = nodes.get(i); |
774 | 742 | if (DEBUG) { |
775 | if (treePath.equals(DEBUG_TREEPATH) || DEBUG_TREEPATH.equals("all")) { | |
743 | if (treePath.equals(DEBUG_TREEPATH) || "all".equals(DEBUG_TREEPATH)) { | |
776 | 744 | for (NodeElem nodeElem : reworked) { |
777 | 745 | nodeElem.saveGPX("debug" + i, treePath); |
778 | 746 | } |
103 | 103 | private int endLevel; |
104 | 104 | private char elevUnits; |
105 | 105 | private int currentLevel; |
106 | private boolean dataHighLevel; | |
107 | private boolean background; | |
106 | 108 | private int poiDispFlag; |
107 | 109 | private String defaultCountry; |
108 | 110 | private String defaultRegion; |
187 | 189 | } |
188 | 190 | |
189 | 191 | /** |
190 | * Get the copyright message. We use whatever was specified inside the | |
191 | * MPF itself. | |
192 | * Get the copyright message. | |
192 | 193 | * |
193 | 194 | * @return A string description of the copyright. |
194 | 195 | */ |
195 | 196 | public String[] copyrightMessages() { |
196 | return new String[] {copyright}; | |
197 | String copyrightFileName = getConfig().getProperty("copyright-file", null); | |
198 | if (copyrightFileName != null) { | |
199 | return readCopyrightFile(copyrightFileName); | |
200 | } | |
201 | if (copyright == null) { | |
202 | copyright = getConfig().getProperty("copyright-message", null); | |
203 | } | |
204 | return new String[] { copyright }; | |
197 | 205 | } |
198 | 206 | |
199 | 207 | /** |
209 | 217 | |
210 | 218 | extraAttributes = null; |
211 | 219 | |
212 | if (name.equalsIgnoreCase("IMG ID")) { | |
220 | if ("IMG ID".equalsIgnoreCase(name)) { | |
213 | 221 | section = S_IMG_ID; |
214 | 222 | poiDispFlag = 0; |
215 | } else if (name.equalsIgnoreCase("POI") || name.equals("RGN10") || name.equals("RGN20")) { | |
223 | } else if ("POI".equalsIgnoreCase(name) || "RGN10".equals(name) || "RGN20".equals(name)) { | |
216 | 224 | point = new MapPoint(); |
217 | 225 | section = S_POINT; |
218 | } else if (name.equalsIgnoreCase("POLYLINE") || name.equals("RGN40")) { | |
226 | } else if ("POLYLINE".equalsIgnoreCase(name) || "RGN40".equals(name)) { | |
219 | 227 | polyline = new MapLine(); |
220 | 228 | roadHelper.clear(); |
221 | 229 | section = S_POLYLINE; |
222 | } else if (name.equalsIgnoreCase("POLYGON") || name.equals("RGN80")) { | |
230 | } else if ("POLYGON".equalsIgnoreCase(name) || "RGN80".equals(name)) { | |
223 | 231 | shape = new MapShape(); |
224 | 232 | section = S_POLYGON; |
225 | } else if (name.equalsIgnoreCase("Restrict")) { | |
233 | } else if ("Restrict".equalsIgnoreCase(name)) { | |
226 | 234 | restriction = new PolishTurnRestriction(); |
227 | 235 | section = S_RESTRICTION; |
228 | 236 | } |
305 | 313 | if (!lineStringMap.isEmpty()) { |
306 | 314 | if (extraAttributes != null && shape.hasExtendedType()) |
307 | 315 | shape.setExtTypeAttributes(makeExtTypeAttributes()); |
316 | if (background && !dataHighLevel) | |
317 | endLevel = levels.length -1; | |
308 | 318 | for (Map.Entry<Integer , List<List<Coord>>> entry : lineStringMap.entrySet()) { |
309 | 319 | setResolution(shape, entry.getKey()); |
310 | 320 | addShapesFromPattern(entry.getValue()); |
328 | 338 | endLevel = 0; |
329 | 339 | lineStringMap.clear(); |
330 | 340 | currentLevel = 0; |
341 | dataHighLevel = false; | |
342 | background = false; | |
331 | 343 | } |
332 | 344 | |
333 | 345 | private void addShapesFromPattern(List<List<Coord>> pointsLists) { |
414 | 426 | * @param value Its value. |
415 | 427 | */ |
416 | 428 | private void point(String name, String value) { |
417 | if (name.equals("Type")) { | |
429 | if ("Type".equals(name)) { | |
418 | 430 | int type = Integer.decode(value); |
419 | 431 | if (type <= 0xff) |
420 | 432 | type <<= 8; |
421 | 433 | point.setType(type); |
422 | 434 | checkType(FeatureKind.POINT, point.getType()); |
423 | } else if (name.equals("SubType")) { | |
435 | } else if ("SubType".equals(name)) { | |
424 | 436 | int subtype = Integer.decode(value); |
425 | 437 | int type = point.getType(); |
426 | 438 | point.setType(type | subtype); |
447 | 459 | * @see #point |
448 | 460 | */ |
449 | 461 | private void line(String name, String value) { |
450 | if (name.equals("Type")) { | |
462 | if ("Type".equals(name)) { | |
451 | 463 | polyline.setType(Integer.decode(value)); |
452 | 464 | checkType(FeatureKind.POLYLINE, polyline.getType()); |
453 | 465 | } else if (name.startsWith("Data")) { |
459 | 471 | (polyline.getType() == 0x22)) { |
460 | 472 | fixElevation(); |
461 | 473 | } |
462 | } else if (name.equals("RoadID")) { | |
474 | } else if ("RoadID".equals(name)) { | |
463 | 475 | if (!routing && roadIdGenerated > 0) |
464 | 476 | throw new MapFailedException("found RoadID without Routing=Y in [IMG ID] section in line " + lineNo); |
465 | 477 | roadHelper.setRoadId(Integer.parseInt(value)); |
466 | 478 | } else if (name.startsWith("Nod")) { |
467 | 479 | roadHelper.addNode(value); |
468 | } else if (name.equals("RouteParam") || name.equals("RouteParams")) { | |
480 | } else if ("RouteParam".equals(name) || "RouteParams".equals(name)) { | |
469 | 481 | roadHelper.setParam(value); |
470 | } else if (name.equals("DirIndicator")) { | |
482 | } else if ("DirIndicator".equals(name)) { | |
471 | 483 | polyline.setDirection(Integer.parseInt(value) > 0); |
472 | 484 | } else if (name.startsWith("Numbers")) { |
473 | 485 | roadHelper.addNumbers(parseNumbers(value)); |
515 | 527 | country = strings[nextPos + 2]; |
516 | 528 | nums.setCityInfo(Numbers.LEFT, createCityInfo(city, region, country)); |
517 | 529 | nextPos = 12; |
518 | } else | |
530 | } else { | |
519 | 531 | nextPos = 10; |
532 | } | |
520 | 533 | city = strings[nextPos]; |
521 | 534 | if (!"-1".equals(city)){ |
522 | 535 | region = strings[nextPos + 1]; |
583 | 596 | * @see #line |
584 | 597 | */ |
585 | 598 | private void shape(String name, String value) { |
586 | if (name.equals("Type")) { | |
599 | if ("Type".equals(name)) { | |
587 | 600 | int type = Integer.decode(value); |
588 | 601 | if (type == 0x4a00) |
589 | 602 | type = 0x4a; |
594 | 607 | } else if (name.startsWith("Data")) { |
595 | 608 | extractResolution(name); |
596 | 609 | addLineString(value, true); |
610 | } else if ("Background".equals(name)) { | |
611 | if ("Y".equals(value)) | |
612 | background = true; | |
597 | 613 | } |
598 | 614 | else { |
599 | 615 | if(extraAttributes == null) |
612 | 628 | } |
613 | 629 | |
614 | 630 | private boolean isCommonValue(MapElement elem, String name, String value) { |
615 | if (name.equals("Label")) { | |
631 | if ("Label".equals(name)) { | |
616 | 632 | elem.setName(unescape(recode(value))); |
617 | } else if (name.equals("Label2") || name.equals("Label3")) { | |
633 | } else if ("Label2".equals(name) || "Label3".equals(name)) { | |
618 | 634 | elem.add2Name(unescape(recode(value))); |
619 | } else if (name.equals("Levels") || name.equals("EndLevel") || name.equals("LevelsNumber")) { | |
635 | } else if ("Levels".equals(name) || "EndLevel".equals(name) || "LevelsNumber".equals(name)) { | |
620 | 636 | try { |
621 | 637 | endLevel = Integer.valueOf(value); |
622 | 638 | } catch (NumberFormatException e) { |
623 | 639 | endLevel = 0; |
624 | 640 | } |
625 | } else if (name.equals("ZipCode")) { | |
641 | } else if ("ZipCode".equals(name)) { | |
626 | 642 | elem.setZip(recode(value)); |
627 | } else if (name.equals("CityName")) { | |
643 | } else if ("CityName".equals(name)) { | |
628 | 644 | elem.setCity(recode(value)); |
629 | } else if (name.equals("StreetDesc")) { | |
645 | } else if ("StreetDesc".equals(name)) { | |
630 | 646 | elem.setStreet(recode(value)); |
631 | } else if (name.equals("HouseNumber")) { | |
647 | } else if ("HouseNumber".equals(name)) { | |
632 | 648 | elem.setHouseNumber(recode(value)); |
633 | } else if (name.equals("is_in")) { | |
649 | } else if ("is_in".equals(name)) { | |
634 | 650 | elem.setIsIn(recode(value)); |
635 | } else if (name.equals("Phone")) { | |
651 | } else if ("Phone".equals(name)) { | |
636 | 652 | elem.setPhone(recode(value)); |
637 | } else if (name.equals("CountryName")) { | |
653 | } else if ("CountryName".equals(name)) { | |
638 | 654 | elem.setCountry(unescape(recode(value))); |
639 | } else if (name.equals("RegionName")) { | |
655 | } else if ("RegionName".equals(name)) { | |
640 | 656 | elem.setRegion(recode(value)); |
641 | 657 | } else { |
642 | 658 | return false; |
764 | 780 | */ |
765 | 781 | private int extractResolution(String name) { |
766 | 782 | currentLevel = Integer.parseInt(name.substring(name.charAt(0) == 'O'? 6: 4)); |
783 | if (currentLevel > 0) | |
784 | dataHighLevel = true; | |
767 | 785 | return extractResolution(currentLevel); |
768 | 786 | } |
769 | 787 | |
794 | 812 | * @param value Command value. |
795 | 813 | */ |
796 | 814 | private void imgId(String name, String value) { |
797 | if (name.equals("Copyright")) { | |
815 | if ("Copyright".equals(name)) { | |
798 | 816 | copyright = value; |
799 | } else if (name.equals("Levels")) { | |
817 | } else if ("Levels".equals(name)) { | |
800 | 818 | int nlev = Integer.parseInt(value); |
801 | 819 | levels = new LevelInfo[nlev]; |
802 | 820 | } else if (name.startsWith("Level")) { |
813 | 831 | char fc = value.charAt(0); |
814 | 832 | if (fc == 'm' || fc == 'M') |
815 | 833 | elevUnits = 'm'; |
816 | } else if (name.equalsIgnoreCase("CodePage")) { | |
834 | } else if ("CodePage".equalsIgnoreCase(name)) { | |
817 | 835 | dec = Charset.forName("cp" + value).newDecoder(); |
818 | 836 | dec.onUnmappableCharacter(CodingErrorAction.REPLACE); |
819 | 837 | } else if (name.endsWith("LeftSideTraffic")){ |
853 | 871 | * @param value A string representing a lat,long pair. |
854 | 872 | * @return The coordinate value. |
855 | 873 | */ |
856 | private Coord makeCoord(String value) { | |
874 | private static Coord makeCoord(String value) { | |
857 | 875 | String[] fields = value.split("[(,)]"); |
858 | 876 | |
859 | 877 | int i = 0; |
872 | 890 | |
873 | 891 | for(Map.Entry<String, String> entry : extraAttributes.entrySet()) { |
874 | 892 | String v = entry.getValue(); |
875 | if (entry.getKey().equals("Depth")) { | |
893 | if ("Depth".equals(entry.getKey())) { | |
876 | 894 | String u = extraAttributes.get("DepthUnit"); |
877 | 895 | if("f".equals(u)) |
878 | 896 | v += "ft"; |
879 | 897 | eta.put("depth", v); |
880 | } else if(entry.getKey().equals("Height")) { | |
898 | } else if("Height".equals(entry.getKey())) { | |
881 | 899 | String u = extraAttributes.get("HeightUnit"); |
882 | 900 | if("f".equals(u)) |
883 | 901 | v += "ft"; |
884 | 902 | eta.put("height", v); |
885 | } else if(entry.getKey().equals("HeightAboveFoundation")) { | |
903 | } else if("HeightAboveFoundation".equals(entry.getKey())) { | |
886 | 904 | String u = extraAttributes.get("HeightAboveFoundationUnit"); |
887 | 905 | if("f".equals(u)) |
888 | 906 | v += "ft"; |
889 | 907 | eta.put("height-above-foundation", v); |
890 | } else if(entry.getKey().equals("HeightAboveDatum")) { | |
908 | } else if("HeightAboveDatum".equals(entry.getKey())) { | |
891 | 909 | String u = extraAttributes.get("HeightAboveDatumUnit"); |
892 | 910 | if("f".equals(u)) |
893 | 911 | v += "ft"; |
894 | 912 | eta.put("height-above-datum", v); |
895 | } else if(entry.getKey().equals("Color")) { | |
913 | } else if("Color".equals(entry.getKey())) { | |
896 | 914 | colour = Integer.decode(v); |
897 | } else if(entry.getKey().equals("Style")) { | |
915 | } else if("Style".equals(entry.getKey())) { | |
898 | 916 | style = Integer.decode(v); |
899 | } else if(entry.getKey().equals("Position")) { | |
917 | } else if("Position".equals(entry.getKey())) { | |
900 | 918 | eta.put("position", v); |
901 | } else if(entry.getKey().equals("FoundationColor")) { | |
919 | } else if("FoundationColor".equals(entry.getKey())) { | |
902 | 920 | eta.put("color", v); |
903 | } else if(entry.getKey().equals("Light")) { | |
921 | } else if("Light".equals(entry.getKey())) { | |
904 | 922 | eta.put("light", v); |
905 | } else if(entry.getKey().equals("LightType")) { | |
923 | } else if("LightType".equals(entry.getKey())) { | |
906 | 924 | eta.put("type", v); |
907 | } else if(entry.getKey().equals("Period")) { | |
925 | } else if("Period".equals(entry.getKey())) { | |
908 | 926 | eta.put("period", v); |
909 | } else if(entry.getKey().equals("Note")) { | |
927 | } else if("Note".equals(entry.getKey())) { | |
910 | 928 | eta.put("note", v); |
911 | } else if(entry.getKey().equals("LocalDesignator")) { | |
929 | } else if("LocalDesignator".equals(entry.getKey())) { | |
912 | 930 | eta.put("local-desig", v); |
913 | } else if(entry.getKey().equals("InternationalDesignator")) { | |
931 | } else if("InternationalDesignator".equals(entry.getKey())) { | |
914 | 932 | eta.put("int-desig", v); |
915 | } else if(entry.getKey().equals("FacilityPoint")) { | |
933 | } else if("FacilityPoint".equals(entry.getKey())) { | |
916 | 934 | eta.put("facilities", v); |
917 | } else if(entry.getKey().equals("Racon")) { | |
935 | } else if("Racon".equals(entry.getKey())) { | |
918 | 936 | eta.put("racon", v); |
919 | } else if(entry.getKey().equals("LeadingAngle")) { | |
937 | } else if("LeadingAngle".equals(entry.getKey())) { | |
920 | 938 | eta.put("leading-angle", v); |
921 | 939 | } |
922 | 940 | } |
934 | 952 | try { |
935 | 953 | // Proceed only if the restriction is not already marked as invalid. |
936 | 954 | if (restriction.isValid()) { |
937 | if (name.equalsIgnoreCase("Nod")) { | |
955 | if ("Nod".equalsIgnoreCase(name)) { | |
938 | 956 | /* ignore */ |
939 | } else if (name.equalsIgnoreCase("TraffPoints")) { | |
957 | } else if ("TraffPoints".equalsIgnoreCase(name)) { | |
940 | 958 | restriction.setTrafficPoints(value); |
941 | } else if (name.equalsIgnoreCase("TraffRoads")) { | |
959 | } else if ("TraffRoads".equalsIgnoreCase(name)) { | |
942 | 960 | restriction.setTrafficRoads(value); |
943 | } else if (name.equalsIgnoreCase("RestrParam")) { | |
961 | } else if ("RestrParam".equalsIgnoreCase(name)) { | |
944 | 962 | restriction.setExceptMask(getRestrictionExceptionMask(value)); |
945 | } else if (name.equalsIgnoreCase("Time")) { | |
963 | } else if ("Time".equalsIgnoreCase(name)) { | |
946 | 964 | log.info("Time in restriction definition is ignored " + restriction); |
947 | 965 | } |
948 | 966 | } |
1029 | 1047 | } |
1030 | 1048 | |
1031 | 1049 | private void checkType(FeatureKind kind, int type) { |
1050 | if (kind == FeatureKind.POLYGON && type == 0x4a) // allow 0x4a polygon for preview map in polish format | |
1051 | return; | |
1032 | 1052 | if (!GType.checkType(kind, type)) { |
1033 | 1053 | throw new MapFailedException("invalid type " + GType.formatType(type) + " for " + kind + ", line " + lineNo); |
1034 | 1054 | } |
0 | /* | |
1 | * Copyright (C) 2019. | |
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.util; | |
13 | ||
14 | ||
15 | import java.awt.geom.Path2D; | |
16 | import java.util.ArrayList; | |
17 | import java.util.BitSet; | |
18 | import java.util.List; | |
19 | import java.util.Set; | |
20 | import java.util.TreeMap; | |
21 | ||
22 | import uk.me.parabola.imgfmt.Utils; | |
23 | import uk.me.parabola.imgfmt.app.Area; | |
24 | import uk.me.parabola.imgfmt.app.Coord; | |
25 | import uk.me.parabola.log.Logger; | |
26 | import uk.me.parabola.mkgmap.reader.osm.Way; | |
27 | ||
28 | /** | |
29 | * Implements insideness tests for points, polyline and polygon. We distinguish | |
30 | * 3 cases for points: <br> | |
31 | * 1: the point is outside the polygon <br> | |
32 | * 2: the point is on the boundary of the polygon (or very close to it) <br> | |
33 | * 3: the point in inside the polygon | |
34 | * | |
35 | * We distinguish 6 cases for lines: <br> | |
36 | * 1: all of the line is outside the polygon <br> | |
37 | * 2: some of the line is outside and the rest touches or runs along the polygon | |
38 | * edge <br> | |
39 | * 3: all of the line runs along the polygon edge <br> | |
40 | * 4: some of the line is inside and the rest touches or runs along. <br> | |
41 | * 5: all of the line is inside the polygon <br> | |
42 | * 6: some is inside and some outside the polygon. Obviously some point is on | |
43 | * the polygon edge but we don't care if runs along the edge. | |
44 | * | |
45 | * @author Gerd Petermann | |
46 | * | |
47 | */ | |
48 | public class IsInUtil { | |
49 | private static final Logger log = Logger.getLogger(IsInUtil.class); | |
50 | public static final int IN = 0x01; | |
51 | public static final int ON = 0x02; | |
52 | public static final int OUT = 0x04; | |
53 | ||
54 | public static final int IN_ON_OUT = IN | ON | OUT; | |
55 | ||
56 | private IsInUtil() { | |
57 | // hide public constructor | |
58 | } | |
59 | ||
60 | public static void mergePolygons(Set<Way> polygons, List<List<Coord>> outers, List<List<Coord>> holes) { | |
61 | // combine all polygons which intersect the bbox of the element if possible | |
62 | Path2D.Double path = new Path2D.Double(); | |
63 | for (Way polygon : polygons) { | |
64 | path.append(Java2DConverter.createPath2D(polygon.getPoints()), false); | |
65 | } | |
66 | java.awt.geom.Area polygonsArea = new java.awt.geom.Area(path); | |
67 | List<List<Coord>> mergedShapes = Java2DConverter.areaToShapes(polygonsArea); | |
68 | ||
69 | // combination of polygons may contain holes. They are counter clockwise. | |
70 | for (List<Coord> shape : mergedShapes) { | |
71 | (Way.clockwise(shape) ? outers : holes).add(shape); | |
72 | } | |
73 | } | |
74 | ||
75 | private enum IntersectionStatus { | |
76 | TOUCHING, CROSSING, SPLITTING, JOINING,SIMILAR, DOUBLE_SPIKE | |
77 | } | |
78 | ||
79 | private static final int EPS_HP = 4; // ~0.15 meters at equator | |
80 | private static final int EPS_HP_SQRD = EPS_HP * EPS_HP; | |
81 | private static final double EPS = 0.15; // meters. needed for distToLineSegment() | |
82 | ||
83 | public static int isLineInShape(List<Coord> lineToTest, List<Coord> shape, Area elementBbox) { | |
84 | final int n = lineToTest.size(); | |
85 | int status = isPointInShape(lineToTest.get(0), shape); | |
86 | BitSet onBoundary = new BitSet(); | |
87 | ||
88 | for (int i = 0; i < shape.size() - 1; i++) { | |
89 | Coord p11 = shape.get(i); | |
90 | Coord p12 = shape.get(i + 1); | |
91 | if (p11.distanceInHighPrecSquared(p12) < EPS_HP_SQRD) { // skip very short segments | |
92 | continue; | |
93 | } | |
94 | // check if shape segment is clearly below, above, right or left of bbox | |
95 | if ((Math.min(p11.getLatitude(), p12.getLatitude()) > elementBbox.getMaxLat() + 1) | |
96 | || (Math.max(p11.getLatitude(), p12.getLatitude()) < elementBbox.getMinLat() - 1) | |
97 | || (Math.min(p11.getLongitude(), p12.getLongitude()) > elementBbox.getMaxLong() + 1) | |
98 | || (Math.max(p11.getLongitude(), p12.getLongitude()) < elementBbox.getMinLong() - 1)) | |
99 | continue; | |
100 | for (int k = 0; k < n - 1; k++) { | |
101 | Coord p21 = lineToTest.get(k); | |
102 | Coord p22 = lineToTest.get(k + 1); | |
103 | if (p21.distanceInHighPrecSquared(p22) < EPS_HP_SQRD) { // skip very short segments | |
104 | continue; | |
105 | } | |
106 | Coord inter = Utils.getSegmentSegmentIntersection(p11, p12, p21, p22); | |
107 | if (inter != null) { | |
108 | // segments have at least one common point | |
109 | boolean isCrossing = false; | |
110 | if (inter.distanceInHighPrecSquared(p21) < EPS_HP_SQRD) { | |
111 | onBoundary.set(k); | |
112 | if (k == 0) { | |
113 | status |= ON; | |
114 | } else { | |
115 | if (p21.distanceInHighPrecSquared(p11) < EPS_HP_SQRD) { | |
116 | Coord p20 = lineToTest.get(k - 1); | |
117 | Coord p10 = shape.get(i - 1 >= 0 ? i - 1 : shape.size() - 2); | |
118 | IntersectionStatus x = analyseCrossingInPoint(p11, p20, p22, p10, p12); | |
119 | Coord pTest = null; | |
120 | if (x == IntersectionStatus.CROSSING) { | |
121 | isCrossing = true; | |
122 | } else if (x == IntersectionStatus.JOINING) { | |
123 | if (!isOnOrCloseToEdgeOfShape(shape, p21, p20)) { | |
124 | pTest = p21.makeBetweenPoint(p20, 0.01); | |
125 | } | |
126 | } else if (x == IntersectionStatus.SPLITTING) { | |
127 | if (!isOnOrCloseToEdgeOfShape(shape, p21, p22)) { | |
128 | pTest = p21.makeBetweenPoint(p22, 0.01); | |
129 | } | |
130 | } | |
131 | if (pTest != null) { | |
132 | int testStat = isPointInShape(pTest, shape); | |
133 | status |= testStat; | |
134 | if ((status|ON) == IN_ON_OUT) | |
135 | return IN_ON_OUT; | |
136 | } | |
137 | } else if (p21.distanceInHighPrecSquared(p12) < EPS_HP_SQRD) { | |
138 | // handled in next iteration (k+1) or (i+1)b | |
139 | } else { | |
140 | // way segment starts on a shape segment | |
141 | // somewhere between p11 and p12 | |
142 | // it may cross the shape or just touch it, | |
143 | // check if previous way segment is on the same | |
144 | // side or not | |
145 | long isLeftPrev = lineToTest.get(k-1).isLeft(p11, p12); | |
146 | long isLeftNext = p22.isLeft(p11, p12); | |
147 | if (isLeftPrev< 0 && isLeftNext > 0 || isLeftPrev > 0 && isLeftNext < 0) { | |
148 | // both way segments are not on the shape | |
149 | // segment and they are on different sides | |
150 | isCrossing = true; | |
151 | } | |
152 | } | |
153 | } | |
154 | } else if (inter.distanceInHighPrecSquared(p22) < EPS_HP_SQRD) { | |
155 | onBoundary.set(k + 1); | |
156 | // handle intersection on next iteration | |
157 | } else if (inter.distanceInHighPrecSquared(p11) < EPS_HP_SQRD || inter.distanceInHighPrecSquared(p12) < EPS_HP_SQRD) { | |
158 | // intersection is very close to end of shape segment | |
159 | if (inter.distToLineSegment(p21, p22) > EPS) | |
160 | isCrossing = true; | |
161 | } else { | |
162 | isCrossing = true; | |
163 | } | |
164 | if (isCrossing) { | |
165 | // real intersection found | |
166 | return IN_ON_OUT; | |
167 | } | |
168 | } | |
169 | } | |
170 | } | |
171 | ||
172 | if (!onBoundary.isEmpty()) | |
173 | status |= ON; | |
174 | if (status == ON) { | |
175 | // found no intersection and first point is on boundary | |
176 | if (onBoundary.cardinality() != n) { | |
177 | // return result for first point which is not on boundary | |
178 | Coord pTest = lineToTest.get(onBoundary.nextClearBit(0)); | |
179 | status |= isPointInShape(pTest, shape); | |
180 | return status; | |
181 | } | |
182 | status |= checkAllOn(lineToTest, shape); | |
183 | } | |
184 | return status; | |
185 | } | |
186 | ||
187 | ||
188 | /** | |
189 | * Handle special case that all points of {@code lineToTest} are on the edge of shape | |
190 | * @param lineToTest | |
191 | * @param shape | |
192 | * @return | |
193 | */ | |
194 | private static int checkAllOn(List<Coord> lineToTest, List<Coord> shape) { | |
195 | int n = lineToTest.size(); | |
196 | // all points are on boundary | |
197 | for (int i = 0; i < n-1; i++) { | |
198 | Coord p1 = lineToTest.get(i); | |
199 | Coord p2 = lineToTest.get(i + 1); | |
200 | if (!isOnOrCloseToEdgeOfShape(shape, p1, p2)) { | |
201 | Coord pTest = p1.makeBetweenPoint(p2, 0.01); | |
202 | int resMidPoint = isPointInShape(pTest, shape); | |
203 | if (resMidPoint != ON) | |
204 | return resMidPoint; | |
205 | } | |
206 | } | |
207 | return ON; | |
208 | } | |
209 | ||
210 | /** | |
211 | * two line-strings a-s-c and x-s-y the same mid point. Check if they are crossing. This is the case | |
212 | * if a-s-c is between x-s-y or if x-s-y is between a-s-c. | |
213 | * @param s the share point | |
214 | * @param a 1st point 1st line-string | |
215 | * @param b 2nd point 1st line-string | |
216 | * @param x 1st point 2nd line-string | |
217 | * @param y 2nd point 2nd line-string | |
218 | * @return kind of crossing or touching | |
219 | */ | |
220 | private static IntersectionStatus analyseCrossingInPoint(Coord s, Coord a, Coord b, Coord x, Coord y) { | |
221 | TreeMap<Long, Character> map = new TreeMap<>(); | |
222 | long ba = Math.round(s.bearingTo(a) * 1000); | |
223 | long bb = Math.round(s.bearingTo(b) * 1000); | |
224 | long bx = Math.round(s.bearingTo(x) * 1000); | |
225 | long by = Math.round(s.bearingTo(y) * 1000); | |
226 | map.put(ba, 'a'); | |
227 | map.put(bb, 'b'); | |
228 | map.put(bx, 'x'); | |
229 | map.put(by, 'y'); | |
230 | List<Character> sortedByBearing = new ArrayList<>(map.values()); | |
231 | int apos = sortedByBearing.indexOf('a'); | |
232 | int bpos = sortedByBearing.indexOf('b'); | |
233 | int xpos = sortedByBearing.indexOf('x'); | |
234 | int ypos = sortedByBearing.indexOf('y'); | |
235 | ||
236 | if (map.size() == 4) { | |
237 | if (Math.abs(xpos-ypos) == 2) { | |
238 | // pair xy is either on 0 and 2 or 1 and 3, so only one of a and b is between them | |
239 | // shape segments x-s-y is nether between nor outside of way segments a-s-b | |
240 | return IntersectionStatus.CROSSING; | |
241 | } | |
242 | return IntersectionStatus.TOUCHING; | |
243 | } | |
244 | ||
245 | if (map.size() == 3) { | |
246 | if (xpos < 0) { | |
247 | // x-s-y is a spike that touches a-s-b | |
248 | return IntersectionStatus.TOUCHING; | |
249 | } | |
250 | if (bpos < 0) { | |
251 | // either s-x or s-y is overlaps s-b | |
252 | return IntersectionStatus.JOINING; | |
253 | } | |
254 | if (ba == bx || ba == by) { | |
255 | return IntersectionStatus.SPLITTING; | |
256 | } | |
257 | return IntersectionStatus.TOUCHING; | |
258 | } | |
259 | if (map.size() == 2) { | |
260 | if (apos > 0 || bpos > 0) { | |
261 | // two spikes meeting | |
262 | return IntersectionStatus.TOUCHING; | |
263 | } | |
264 | // a-s-b and x-s-y are overlapping (maybe have different directions) | |
265 | return IntersectionStatus.SIMILAR; | |
266 | } | |
267 | // both a-s-b and x-s-y come from and go to the same direction | |
268 | return IntersectionStatus.DOUBLE_SPIKE; | |
269 | } | |
270 | ||
271 | /** | |
272 | * Check if the sequence p1-p2 or p2-p1 appears in the shape or if there is only one point c between and the sequence p1-c-p2 | |
273 | * is nearly straight. | |
274 | * @param shape list of points describing the shape | |
275 | * @param p1 first point | |
276 | * @param p2 second point | |
277 | * @return true if the sequence p1-p2 or p2-p1 appears in the shape or if there is only one point c between and the sequence p1-c-p2 | |
278 | * is nearly straight, else false. | |
279 | */ | |
280 | private static boolean isOnOrCloseToEdgeOfShape(List<Coord> shape, Coord p1, Coord p2) { | |
281 | for (int i = 0; i < shape.size(); i++) { | |
282 | Coord p = shape.get(i); | |
283 | if (p.distanceInHighPrecSquared(p1) >= EPS_HP_SQRD) | |
284 | continue; | |
285 | ||
286 | int posPrev = i > 0 ? i - 1 : shape.size() - 2; | |
287 | int posNext = i < shape.size() - 1 ? i + 1 : 1; | |
288 | if (shape.get(posPrev).distanceInHighPrecSquared(p2) < EPS_HP_SQRD || shape.get(posNext).distanceInHighPrecSquared(p2) < EPS_HP_SQRD) | |
289 | return true; | |
290 | ||
291 | int posPrev2 = posPrev > 0 ? posPrev - 1 : shape.size() - 2; | |
292 | int posNext2 = posNext < shape.size() - 1 ? posNext + 1 : 1; | |
293 | if (shape.get(posPrev2).distanceInHighPrecSquared(p2) < EPS_HP_SQRD && Math.abs(Utils.getAngle(p1, shape.get(posPrev), p2)) < 0.1) { | |
294 | // shape segments between p1 and p2 are almost straight | |
295 | return true; | |
296 | } | |
297 | if (shape.get(posNext2).distanceInHighPrecSquared(p2) < EPS_HP_SQRD && Math.abs(Utils.getAngle(p1, shape.get(posNext), p2)) < 0.1) { | |
298 | // shape segments between p1 and p2 are almost straight | |
299 | return true; | |
300 | } | |
301 | } | |
302 | ||
303 | return false; | |
304 | } | |
305 | ||
306 | /** | |
307 | * Check if node is in polygon using crossing number counter, with some some tolerance | |
308 | * @param node the point to test | |
309 | * @param shape list of points describing the polygon | |
310 | * @return IN/ON/OUT | |
311 | */ | |
312 | public static int isPointInShape(Coord node, List<Coord> shape) { | |
313 | final int nodeLat = node.getHighPrecLat(); | |
314 | final int nodeLon = node.getHighPrecLon(); | |
315 | if (log.isDebugEnabled()) { | |
316 | log.debug("node ", node, nodeLon, nodeLat, shape.size(), shape); | |
317 | } | |
318 | int trailLat = 0, trailLon = 0; | |
319 | int lhsCount = 0, rhsCount = 0; // count both, to be sure | |
320 | int minLat, maxLat, minLon, maxLon; | |
321 | double lonDif, latDif, distSqrd; | |
322 | boolean subsequent = false; | |
323 | for (Coord leadCoord : shape) { | |
324 | final int leadLat = leadCoord.getHighPrecLat(); | |
325 | final int leadLon = leadCoord.getHighPrecLon(); | |
326 | if (subsequent) { // use first point as trailing (poly is closed) | |
327 | if (leadCoord.distanceInHighPrecSquared(node) < EPS_HP_SQRD) | |
328 | return ON; | |
329 | if (leadLat < trailLat) { | |
330 | minLat = leadLat; | |
331 | maxLat = trailLat; | |
332 | } else { | |
333 | minLat = trailLat; | |
334 | maxLat = leadLat; | |
335 | } | |
336 | if (leadLon < trailLon) { | |
337 | minLon = leadLon; | |
338 | maxLon = trailLon; | |
339 | } else { | |
340 | minLon = trailLon; | |
341 | maxLon = leadLon; | |
342 | } | |
343 | if (minLat - EPS_HP > nodeLat) { | |
344 | // line segment is all slightly above, ignore | |
345 | } else if (maxLat + EPS_HP < nodeLat) { | |
346 | // line segment is all slightly below, ignore | |
347 | } else if (minLon - EPS_HP > nodeLon && minLat < nodeLat && maxLat > nodeLat) { | |
348 | ++rhsCount; // definite line segment all slightly to the right | |
349 | } else if (maxLon + EPS_HP < nodeLon && minLat < nodeLat && maxLat > nodeLat) { | |
350 | ++lhsCount; // definite line segment all slightly to the left | |
351 | } else { // need to consider this segment more carefully. | |
352 | if (leadLat == trailLat) | |
353 | lonDif = 0; // dif meaningless; will be ignored in crossing calc, 0 handled for distToLine calc | |
354 | else | |
355 | lonDif = nodeLon - trailLon - (double)(nodeLat - trailLat) / (leadLat - trailLat) * (leadLon - trailLon); | |
356 | if (leadLon == trailLon) | |
357 | latDif = 0; // ditto | |
358 | else | |
359 | latDif = nodeLat - trailLat - (double)(nodeLon - trailLon) / (leadLon - trailLon) * (leadLat - trailLat); | |
360 | // calculate distance to segment using right-angle attitude theorem | |
361 | final double lonDifSqrd = lonDif*lonDif; | |
362 | final double latDifSqrd = latDif*latDif; | |
363 | log.debug("inBox", leadLon-nodeLon, leadLat-nodeLat, trailLon-nodeLon, trailLat-nodeLat, lonDif, latDif, lhsCount, rhsCount); | |
364 | // there a small area between the square EPS_HP*2 and the circle within, where, if polygon vertix and | |
365 | // segments are the other side, it might still be calculated as ON. | |
366 | if (lonDif == 0) | |
367 | distSqrd = latDifSqrd; | |
368 | else if (latDif == 0) | |
369 | distSqrd = lonDifSqrd; | |
370 | else | |
371 | distSqrd = lonDifSqrd * latDifSqrd / (lonDifSqrd + latDifSqrd); | |
372 | if (distSqrd < EPS_HP_SQRD) | |
373 | return ON; | |
374 | if ((trailLat <= nodeLat && leadLat > nodeLat) || // an upward crossing | |
375 | (trailLat > nodeLat && leadLat <= nodeLat)) { // a downward crossing | |
376 | if (lonDif < 0) | |
377 | ++rhsCount; // a valid crossing right of nodeLon | |
378 | else | |
379 | ++lhsCount; | |
380 | } | |
381 | } | |
382 | } // if not first Coord | |
383 | subsequent = true; | |
384 | trailLat = leadLat; | |
385 | trailLon = leadLon; | |
386 | } // for leadCoord | |
387 | log.debug("lhs | rhs", lhsCount, rhsCount); | |
388 | assert (lhsCount & 1) == (rhsCount & 1) : "LHS: " + lhsCount + " RHS: " + rhsCount; | |
389 | return (rhsCount & 1) == 1 ? IN : OUT; | |
390 | } | |
391 | ||
392 | } |
86 | 86 | } |
87 | 87 | |
88 | 88 | @Test |
89 | public void destOnRhumLineAt180(){ | |
90 | Coord russia3 = russia1.destOnRhumLine(1, 0.0); | |
89 | public void destOnRhumbLineAt180(){ | |
90 | Coord russia3 = russia1.destOnRhumbLine(1, 0.0); | |
91 | 91 | assertEquals(russia3.getLongitude(), russia1.getLongitude()); |
92 | Coord russia4 = russia1.destOnRhumLine(10000, 0.0); | |
92 | Coord russia4 = russia1.destOnRhumbLine(10000, 0.0); | |
93 | 93 | assertEquals(russia4.getLongitude(), russia1.getLongitude()); |
94 | 94 | } |
95 | 95 |
473 | 473 | List<MapShape> res = smf.merge(Arrays.asList(s1,s2)); |
474 | 474 | assertTrue(testId, res != null); |
475 | 475 | assertEquals(testId,expectedNumShapes, res.size() ); |
476 | // if (res.get(0).getPoints().size() != expectedNumPoints){ | |
477 | // GpxCreator.createGpx("e:/ld/s1", s1.getPoints()); | |
478 | // GpxCreator.createGpx("e:/ld/s2", s2.getPoints()); | |
479 | // GpxCreator.createGpx("e:/ld/res", res.get(0).getPoints()); | |
480 | // } | |
481 | 476 | assertEquals(testId, expectedNumPoints, res.get(0).getPoints().size()); |
482 | 477 | // TODO: test shape size |
483 | 478 | } |
205 | 205 | } |
206 | 206 | |
207 | 207 | @Test |
208 | public void testTwoFunctionsDifferentComplexity() { | |
209 | Op op = createOp("a=1 & is_in(landuse,residential,all)=true & length()< 10 [0x02]"); | |
210 | op = arranger.arrange(op); | |
211 | ||
212 | String formatted = fmtExpr(op); | |
213 | System.out.println(formatted); | |
214 | int posLen = formatted.indexOf("length"); | |
215 | int posIsIn = formatted.indexOf("is_in"); | |
216 | assertTrue(posLen >= 0); | |
217 | assertTrue(posIsIn >= 0); | |
218 | assertTrue(posLen < posIsIn); | |
219 | } | |
220 | ||
221 | @Test | |
208 | 222 | public void testEqualTagValue() { |
209 | 223 | Op op = createOp("c!=d & a=$b"); |
210 | 224 | op = arranger.arrange(op); |
0 | /* | |
1 | * Copyright (C) 2020. | |
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 | ||
13 | package uk.me.parabola.util; | |
14 | ||
15 | import static org.junit.Assert.assertTrue; | |
16 | ||
17 | import java.io.FileNotFoundException; | |
18 | import java.io.IOException; | |
19 | import java.io.InputStream; | |
20 | import java.util.ArrayList; | |
21 | import java.util.Arrays; | |
22 | import java.util.Collections; | |
23 | import java.util.HashMap; | |
24 | import java.util.LinkedHashSet; | |
25 | import java.util.List; | |
26 | import java.util.Map; | |
27 | import java.util.Set; | |
28 | import java.util.stream.Collectors; | |
29 | ||
30 | import org.junit.Test; | |
31 | ||
32 | import func.lib.Args; | |
33 | import uk.me.parabola.imgfmt.Utils; | |
34 | import uk.me.parabola.imgfmt.app.Area; | |
35 | import uk.me.parabola.imgfmt.app.Coord; | |
36 | import uk.me.parabola.mkgmap.osmstyle.function.IsInFunction; | |
37 | import uk.me.parabola.mkgmap.reader.osm.Element; | |
38 | import uk.me.parabola.mkgmap.reader.osm.FeatureKind; | |
39 | import uk.me.parabola.mkgmap.reader.osm.Node; | |
40 | import uk.me.parabola.mkgmap.reader.osm.OsmMapDataSource; | |
41 | import uk.me.parabola.mkgmap.reader.osm.Way; | |
42 | ||
43 | /* | |
44 | Source: test/resources/in/osm/is-in-samples.osm | |
45 | errors: test-reports/uk/me/parabola/util/62_IsInUtilTest-err.html | |
46 | */ | |
47 | ||
48 | public class IsInUtilTest { | |
49 | ||
50 | Area testSourceBbox = null; | |
51 | ||
52 | private static final String allPointMethods = "in,in_or_on,on"; | |
53 | private static final String allLineMethods = "all,all_in_or_on,on,any,none"; | |
54 | private static final String allPolygonMethods = "all,any"; | |
55 | ||
56 | private static final Map<Integer, String> pointMethods = new HashMap<>(); | |
57 | private static final Map<Integer, String> lineMethods = new HashMap<>(); | |
58 | private static final Map<Integer, String> polygonMethods = new HashMap<>(); | |
59 | ||
60 | public IsInUtilTest() { | |
61 | // set up the methods that should return true for the 'expected' value | |
62 | pointMethods.put(1, "in,in_or_on"); | |
63 | pointMethods.put(2, "in_or_on,on"); | |
64 | pointMethods.put(4, ""); | |
65 | ||
66 | /* all=someInNoneOut, any=anyIn, none=someOutNoneIn | |
67 | 1 2 4 | |
68 | a 1) IN all allInOrOn any | |
69 | b 3) IN ON all allInOrOn any | |
70 | c 7) IN ON OUT any | |
71 | d 2) ON allInOrOn on | |
72 | e 6) ON OUT none | |
73 | f 4) OUT none | |
74 | */ | |
75 | lineMethods.put(1, "all,all_in_or_on,any"); | |
76 | lineMethods.put(2, "all_in_or_on,on"); | |
77 | lineMethods.put(3, "all,all_in_or_on,any"); | |
78 | lineMethods.put(4, "none"); | |
79 | //lineMethods.put(5, ""); | |
80 | lineMethods.put(6, "none"); | |
81 | lineMethods.put(7, "any"); | |
82 | ||
83 | polygonMethods.put(1, "all,any"); | |
84 | polygonMethods.put(2, "all,any"); | |
85 | polygonMethods.put(3, "all,any"); | |
86 | polygonMethods.put(4, ""); | |
87 | //polygonMethods.put(5, ""); | |
88 | polygonMethods.put(6, ""); | |
89 | polygonMethods.put(7, "any"); | |
90 | } | |
91 | ||
92 | private static boolean invokeMethod(IsInFunction anInst, String method, FeatureKind kind, Element el) { | |
93 | anInst.setParams(Arrays.asList("landuse", "residential", method), kind); // tag key/value don't matter | |
94 | String rslt = anInst.calcImpl(el); | |
95 | return "true".equals(rslt); | |
96 | } | |
97 | ||
98 | public List<String> testWithVariants(FeatureKind kind, Element el, String name, Set<Way> polygons) { | |
99 | List<String> errors = new ArrayList<>(); | |
100 | ||
101 | IsInFunction anInst = new IsInFunction(); | |
102 | List<Element> matchingPolygons = new ArrayList<>(); | |
103 | for (Way polygon : polygons) | |
104 | matchingPolygons.add(polygon); | |
105 | anInst.unitTestAugment(new ElementQuadTree(testSourceBbox, matchingPolygons)); | |
106 | ||
107 | String expectedVal = el.getTag("expected"); | |
108 | if (expectedVal != null && !"?".equals(expectedVal)) { | |
109 | int expected = Integer.parseInt(expectedVal); | |
110 | String allMethods = ""; | |
111 | Map<Integer, String> methods = null; | |
112 | switch (kind) { | |
113 | case POINT: | |
114 | allMethods = allPointMethods; | |
115 | methods = pointMethods; | |
116 | break; | |
117 | case POLYLINE: | |
118 | allMethods = allLineMethods; | |
119 | methods = lineMethods; | |
120 | break; | |
121 | case POLYGON: | |
122 | allMethods = allPolygonMethods; | |
123 | methods = polygonMethods; | |
124 | break; | |
125 | } | |
126 | if (!methods.containsKey(expected)) { | |
127 | errors.add(name + " failed, no methods for expected: " + expectedVal); | |
128 | return errors; | |
129 | } | |
130 | String[] trueMethods = methods.get(expected).split(","); | |
131 | if (trueMethods[0].isEmpty()) | |
132 | trueMethods = new String[0]; | |
133 | List<String> falseMethods = new ArrayList<>(); | |
134 | for (String tstMethod : allMethods.split(",")) { | |
135 | boolean inList = false; | |
136 | for (String trueMethod : trueMethods) | |
137 | if (tstMethod.equals(trueMethod)) { | |
138 | inList = true; | |
139 | break; | |
140 | } | |
141 | if (!inList) | |
142 | falseMethods.add(tstMethod); | |
143 | } | |
144 | ||
145 | for (String tstMethod : trueMethods) | |
146 | if (!invokeMethod(anInst, tstMethod, kind, el)) | |
147 | errors.add(name + " failed, expected: " + expectedVal + ". " + tstMethod + " should be true"); | |
148 | for (String tstMethod : falseMethods) | |
149 | if (invokeMethod(anInst, tstMethod, kind, el)) | |
150 | errors.add(name + " failed, expected: " + expectedVal + ". " + tstMethod + " should be false"); | |
151 | ||
152 | if (!errors.isEmpty() || !(el instanceof Way)) | |
153 | return errors; | |
154 | Way w2 = (Way) el.copy(); | |
155 | Collections.reverse(w2.getPoints()); | |
156 | for (String tstMethod : trueMethods) | |
157 | if (!invokeMethod(anInst, tstMethod, kind, w2)) | |
158 | errors.add(name + " failed reversed, expected: " + expectedVal + ". " + tstMethod + " should be true"); | |
159 | for (String tstMethod : falseMethods) | |
160 | if (invokeMethod(anInst, tstMethod, kind, w2)) | |
161 | errors.add(name + " failed reversed, expected: " + expectedVal + ". " + tstMethod + " should be false"); | |
162 | ||
163 | if (!errors.isEmpty() || !w2.hasIdenticalEndPoints()) | |
164 | return errors; | |
165 | List<Coord> points = w2.getPoints(); | |
166 | for (int i = 1; i < w2.getPoints().size(); i++) { | |
167 | points.remove(points.size() - 1); | |
168 | Collections.rotate(points, 1); | |
169 | points.add(points.get(0)); | |
170 | for (String tstMethod : trueMethods) | |
171 | if (!invokeMethod(anInst, tstMethod, kind, w2)) | |
172 | errors.add(name + " failed rotated, expected: " + expectedVal + ". " + tstMethod + " should be true"); | |
173 | for (String tstMethod : falseMethods) | |
174 | if (invokeMethod(anInst, tstMethod, kind, w2)) | |
175 | errors.add(name + " failed rotated, expected: " + expectedVal + ". " + tstMethod + " should be false"); | |
176 | } | |
177 | } | |
178 | return errors; | |
179 | } | |
180 | ||
181 | @Test | |
182 | public void testBasic() throws FileNotFoundException { | |
183 | ||
184 | // just loads the file | |
185 | class TestSource extends OsmMapDataSource { | |
186 | @Override | |
187 | public Set<String> getUsedTags() { | |
188 | // return null => all tags are used | |
189 | return null; | |
190 | } | |
191 | ||
192 | @Override | |
193 | public void load(String name, boolean addBackground) throws FileNotFoundException { | |
194 | try (InputStream is = Utils.openFile(name)) { | |
195 | parse(is, name); | |
196 | } catch (IOException e) { | |
197 | // exception thrown from implicit call to close() on resource variable 'is' | |
198 | } | |
199 | ||
200 | elementSaver.finishLoading(); | |
201 | } | |
202 | } | |
203 | TestSource src = new TestSource(); | |
204 | src.config(new EnhancedProperties()); | |
205 | src.load(Args.TEST_RESOURCE_OSM + "is-in-samples.osm", false); | |
206 | testSourceBbox = src.getElementSaver().getBoundingBox(); | |
207 | ||
208 | ElementQuadTree qt = IsInFunction.buildTree(src.getElementSaver(), "landuse", "residential"); | |
209 | ArrayList<String> allErrors = new ArrayList<>(); | |
210 | for (Node n: src.getElementSaver().getNodes().values()) { | |
211 | String name = n.getTag("name"); | |
212 | if (name != null) { | |
213 | Area elementBbox = Area.getBBox(Collections.singletonList((n).getLocation())); | |
214 | Set<Way> polygons = qt.get(elementBbox).stream().map(e -> (Way) e) | |
215 | .collect(Collectors.toCollection(LinkedHashSet::new)); | |
216 | allErrors.addAll(testWithVariants(FeatureKind.POINT, n, name, polygons)); | |
217 | } | |
218 | } | |
219 | for (Way w: src.getElementSaver().getWays().values()) { | |
220 | String name = w.getTag("name"); | |
221 | if (name != null) { | |
222 | Area elementBbox = Area.getBBox(w.getPoints()); | |
223 | Set<Way> polygons = qt.get(elementBbox).stream().map(e -> (Way) e) | |
224 | .collect(Collectors.toCollection(LinkedHashSet::new)); | |
225 | if (name.startsWith("w")) | |
226 | allErrors.addAll(testWithVariants(FeatureKind.POLYLINE, w, name, polygons)); | |
227 | else | |
228 | allErrors.addAll(testWithVariants(FeatureKind.POLYGON, w, name, polygons)); | |
229 | } | |
230 | } | |
231 | for (String msg : allErrors) { | |
232 | System.err.println(msg); | |
233 | } | |
234 | assertTrue("Found errors. Check System.err content", allErrors.isEmpty()); | |
235 | } | |
236 | } |