Codebase list mkgmap / 56296be
New upstream version 0.0.0+svn4475 Bas Couwenberg 4 years ago
45 changed file(s) with 2093 addition(s) and 755 deletion(s). Raw diff Collapse all Expand all
311311 ignoreerrors="true"/>
312312 <get src="http://www.mkgmap.org.uk/testinput/osm/uk-test-2.osm.gz"
313313 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"
314317 ignoreerrors="true"/>
315318 <get src="http://www.mkgmap.org.uk/testinput/mp/test1.mp"
316319 dest="test/resources/in/mp/test1.mp" usetimestamp="true"
1818 need to use this option to allow more memory to be allocated to the Java
1919 heap. Typically, mkgmap requires about 500MB per core, so an 8-core
2020 processor might need to specify -Xmx4g - note there is no space or
21 equals sign in the option.
21 equals sign in the option.
2222
2323 ;-enableassertions
24 ;-ea
2425 : Causes an error to be thrown if an assertion written in the mkgmap
2526 code is evaluated as not true. This is useful in detecting bugs in the
2627 mkgmap code.
3435 === Mkgmap options ===
3536
3637 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
3839 will need to put most of your options before '-c template.args'
3940 (this file is generated by splitter).
4041
4647 : Display help on the given topic. If the topic is omitted then
4748 general help information is displayed, the same as in help=help.
4849
49 ;--version
50 ;--version
5051 : Write program version to stderr.
5152
5253 === File options ===
5455 ;filename
5556 ;--input-file=filename
5657 : 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
5859 wanted options before this.
5960
6061 ;--gmapsupp
7879 <p>
7980 Lines beginning with a # character are ignored and can be used as
8081 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.
8384
8485 ;--output-dir=directory
8586 : Specify the directory in which all output files are written. It defaults
143144 gmapsupp.img file so that address search will work on a GPS
144145 device.
145146 <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
147148 are given alongside the --index option, then both indexes will be created.
148149 Note that this will require roughly twice as much memory.
149150 <p>
187188 of special characters in the road labels to mark the beginning and end of
188189 the important part. In combination with option --split-name-index
189190 only the words in the important part are indexed.
190 <p>
191
191192 :There are two main effects of this option:
192193 :: - On the PC, when zooming out, the name 'Rue de la Concorde' is only
193194 rendered as 'Concorde'.
194195 :: - The index for road names only contains the important part of the name.
195196 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.
198199 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.
201202 : See comments in the sample roadNameConfig.txt for further details.
202203
203204 ;--mdr7-excl=name[,name...]
220221 :: - 0x00 .. 0x0f (cities, sub type 0, type <= 0xf)
221222 :: - 0x2axx..0x30xx (Food & Drink, Lodging, ...)
222223 :: - 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.
225226 The excluded types are not indexed, but may still be searchable on a device,
226227 as some devices seem to ignore most of the index, e.g. an Oregon 600 with
227228 firmware 5.00 only seems to use it for city search.
228229 If your device finds a POI name like 'Planet' when you search for 'Net',
229230 it doesn't use the index because the index created by mkgmap cannot help for
230231 that search.
231 <p>
232
232233 : 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.
234235 The option expects a comma separated list of types or type ranges. A range is
235236 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:
238239 :: - Assume your style adds a POI with type 0x2800 for each addr:housenumber.
239240 It is not useful to index those numbers, so you can use --poi-excl-index=0x2800
240241 to exclude this.
252253 <p>
253254 The following special tags are added:
254255 <pre>
255 mkgmap:admin_level2 : Name of the admin_level=2 boundary
256 mkgmap:admin_level2 : Name of the admin_level=2 boundary
256257 mkgmap:admin_level3 : Name of the admin_level=3 boundary
257258 ..
258259 mkgmap:admin_level11
264265 uk.me.parabola.mkgmap.reader.osm.boundary.BoundaryPreprocessor
265266 <inputfile> <boundsdir>
266267 </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
269270 contains the boundary data only to avoid very high memory usage.
270271 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.
272273
273274 ;--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
275276 are gathered automatically if the fields are not set by using the special
276277 mkgmap address tags (e.g. mkgmap:city - see option index).
277278 Warning: automatic assignment of address fields is somehow a best guess.
278279 :;is_in
279280 :: The is_in tag is analysed for country and region information.
280 <p>
281281 :;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.
286286
287287 ;--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
290290 to streets. A match between a house number element and a street is created if
291291 the street is located within a radius of 150m and the addr:street tag value of
292292 the house number element equals the mgkmap:street tag value of the street.
293293 The mkgmap:street tag must be added to the street in the style file.
294294 For optimal results, the tags mkgmap:city and mkgmap:postal_code should be
295295 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
297297 same number.
298 : Example for given street name:
298 : Example for given street name:
299299 :: Node - addr:street=Main Street addr:housenumber=2
300300 :: Way 1 - name=Main Street
301301 :: Way 2 - name=Main Street, mkgmap:street=Main Street
323323
324324 ;--overview-levels=level:resolution[,level:resolution...]
325325 : 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
328328 seems to be 11. The hard coded default is empty.
329 : See also option --overview-dem-dist.
329 : See also option --overview-dem-dist.
330330
331331 ;--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.
334334 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.
337337
338338 === Style options ===
339339
356356 style file.
357357
358358 ;--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.
362362
363363 ;--style-option=tag[=value][;tag[=value]...]
364364 : Provide a semicolon separated list of tags which can be used in the style.
365365 The intended use is to make a single style more flexible, e.g.
366366 you may want to use a slightly different set of rules for a map of
367367 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.
369369 This option allows to use rules like
370370 mkgmap:option:light=true & landuse=farmland {remove landuse}
371371 Example: -- style-option=light;routing=car
372372 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.
374374
375375 ;--list-styles
376376 : List the available styles. If this option is preceded by a style-file
377377 option then it lists the styles available within that file.
378378
379379 ;--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
382382 available within that file. If it is also preceded by the style
383383 option it will only check that style.
384384
448448 : Simplifies the ways with the Douglas Peucker algorithm.
449449 NUM is the maximal allowed error distance, by which the resulting
450450 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.
452452 Recommended setting is 4, this should lead to only small differences
453453 (Default is 2.6, which should lead to invisible changes)
454454
464464
465465 ;--min-size-polygon=NUM
466466 : 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.
468468 Recommended value is 8 to 15, default is 8.
469469 : See also polygon-size-limits.
470470
471471 ;--polygon-size-limits=resolution:value[,resolution:value...]
472472 : Allows you to specify different min-size-polygon values for each resolution.
473 Sample:
473 Sample:
474474 :: --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
476476 one. For the given sample, resolutions 19 to 24 will use value 12,
477477 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.
479479 Note that in resolution 24 the filter is not used.
480480 The following options are equivalent:
481481 :: --min-size-polygon=12
498498 formula sqrt(size/2) gives an integer value.
499499
500500 ;--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
503503 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
505505 make sure that they are found first. There are different sources for *.hgt
506506 files, some have so called voids which are areas without data. Those should be
507507 avoided.
516516 distance between two points in the hgt file, that is 3314 for 1'' and 9942 for
517517 3''. Higher distances mean lower resolution and thus fewer bytes in the map.
518518 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
520520 files (3312 is often used), and 5520 to 9942 for 3'' hgt input files.
521521 : Example which should work with levels="0:24, 1:22, 2:20, 3:18":
522522 : --dem-dists=3312,13248,26512,53024
531531 resolution and dem-dist value, else bilinear is used. The default is auto.
532532
533533 ;--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
535535 file format. The polygon described in the file is used to determine the area
536536 for which DEM data should be added to the map. If not given, the DEM data will
537537 cover the full tile area.
541541 overview map. If not given or 0, mkgmap will not add DEM to the overview map.
542542 Reasonable values depend on the size of the area and the lowest resolution
543543 used for the single tiles, good compromises are somewhere between 55000
544 and 276160.
544 and 276160.
545545
546546 === Miscellaneous options ===
547547
569569 : Tells mkgmap to write NET data, which is needed for address search
570570 and routing. Use this option if you want address search, but do
571571 not need a map that supports routing or house number search.
572 <p>
572
573573 ;--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
575575 that support routing. If you specify this option, you do not need
576576 to specify --net and --no-net is ignored.
577577
595595
596596 ;--drive-on=left|right|detect|detect,left|detect,right
597597 : 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
600600 to find out the proper flag. If that detection
601601 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
603603 the country each road is in and compares the number
604604 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.
607607
608608 ;--check-roundabouts
609609 : Check that roundabouts have the expected direction (clockwise
660660 ;--cycle-map
661661 : Tells mkgmap that the map is for cyclists. This assumes that
662662 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
664664 good for racing bikes, but not for other cyclists.
665665 This allows the optimisation of sharp angles at junctions of those roads.
666666 Don't use with the default style as that is a general style!
674674 ;--report-dead-ends=LEVEL
675675 : Set the dead end road warning level. The value of LEVEL (which
676676 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.
680688
681689 ;--add-pois-to-lines
682690 : Generate POIs for lines. For each line (must not be closed) POIs are
686694 the following values:
687695 :* start - The first point of the line
688696 :* 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
690698 :* mid - The middle point
699
691700 ;--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
694703 POI is tagged with the same tags of
695704 the area/multipolygon. Additionally the tag mkgmap:area2poi=true is
696705 set so that it is possible to use that information in the points style
697706 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:
701709 ::* 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:
705712 ::* the node with role=label
706713 ::* the centre point of the biggest area
714
707715 ;--pois-to-areas-placement=tag=value[;tag=value...]
708716 : A POI is placed at the first node of the polygon tagged with the first tag/value
709717 pair. If none of the nodes are tagged with the first tag-value pair the first node
714722 Default: entrance=main;entrance=yes;building=entrance
715723
716724 ;--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.
718726 Sea files in a zip file must be located in the zip file's root directory or in
719727 a sub directory sea. When this option is defined all natural=coastline tags
720728 from the input OSM tiles are removed and the precompiled data is used instead.
722730 and land-tag. The coastlinefile option is ignored if precomp-sea is set.
723731
724732 ;--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
727735 this option is set. Files must have OSM or PBF file format.
728736
729737 ;--generate-sea[=ValueList]
753761 :;close-gaps=NUM
754762 :: close gaps in coastline that are less than this distance (metres)
755763
756 :;floodblocker
764 :;floodblocker
757765 :: enable the flood blocker that prevents a flooding of
758766 land by checking if the sea polygons contain streets
759767 (works only with multipolygon processing)
760768
761 :;fbgap=NUM
769 :;fbgap=NUM
762770 :: flood blocker gap in metre (default 40)
763771 points that are closer to the sea polygon do not block
764772
765773 :;fbthres=NUM
766 :: at least so many highway points must be contained in
774 :: at least so many highway points must be contained in
767775 a sea polygon so that it may be removed by the flood
768776 blocker (default 20)
769777
770778 :;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
773781 (default 0.5)
774782
775783 :;fbdebug
791799 the original that allows bicycle traffic (in both directions).
792800
793801 ;--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
795803 to a small part of the way the POI is located on. This can be used
796804 to let barriers block a way or to lower the calculated speed
797805 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)
799807 or barrier=* (e.g. barrier=cycle_barrier) are supported.
800808 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
807815 applied to a small part of the way around the POI, typically
808816 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.
810818
811819 ;--process-destination
812820 : Splits all motorway_link, trunk_link, primary_link, secondary_link,
813821 and tertiary_link ways tagged with destination into two or three parts where
814822 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,
816824 destination:street and some variants with :forward/:backward like
817825 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
819827 it and the way is split.
820828 This happens before the style rules are processed.
821829 This allows to use any routable Garmin type (except 0x08 and 0x09)
824832 : See also --process-exits.
825833
826834 ;--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
828836 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.
832840 Additionally the middle part is tagged with the following tags:
833841
834842 :: mkgmap:exit_hint=true
837845 :: mkgmap:exit_hint_exit_to=<exit_to tag value of the exit>
838846
839847 : 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
842850 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.
844852
845853 ;--delete-tags-file=filename
846854 : Names a file that should contain one or more lines of the form
856864 an overview map. The options --nsis and --gmapi imply --tdbfile.
857865
858866 ;--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
864872 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.
866874 : The default is show-profiles=0.
867875
868876 ;--transparent
884892
885893 ;--hide-gmapsupp-on-pc
886894 : 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
888896 from the device.
889897
890898 ;--poi-address
902910 that smaller features are rendered over larger ones
903911 (assuming the draw order is equal).
904912 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.
906914
907915 === Deprecated and Obsolete Options ===
908916
909917 ;--drive-on-left
910918 ;--drive-on-right
911919 : 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.
913921
914922 ;--make-all-cycleways
915923 : Deprecated; use --make-opposite-cycleways instead. Former meaning:
966974
967975 : Optional BITMASK (default value 3) allows you to specify which
968976 adjustments are to be made (where necessary):
969
970977 :: 1 = increase angle between side road and outgoing main road
971978 :: 2 = increase angle between side road and incoming main road
1313 People who have contributed suggestions and corrections to this document
1414 are:
1515 Carlos Dávila,
16 Geoff Sherlock
17
16 Geoff Sherlock,
17 Ticker Berkin
1818
1919 The list of nicknames of everyone that had modified the wiki pages at the time that
2020 this manual was created is as follows:
+0
-52
doc/styles/main.txt less more
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[]
243243 Functions calculate a specific property of an OSM element.
244244
245245 .Style functions
246 [width="100%",cols="2,1,1,1,5",options="header"]
246 [width="100%",cols="5,1,1,1,11",options="header"]
247247 |=====
248 |Function |Node |Way |Relation |Description
248 |Function |Node |Way |Rel. |Description
249249 |length() | | x | x |
250250 Calculates the length in m. For relations its the sum of all member length (including sub relations).
251251
280280 |osmid() | x | x | x |
281281 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.
282282 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.
283308
284309 |====
285310
00 = Conversion Style manual
11 The mkgmap team
2 :pubdate: January 2013
2 :pubdate: set to date of pdf generation
33 :toc:
44 :numbered:
55 :doctype: book
77 :email: mkgmap-dev@lists.mkgmap.org.uk
88 :description: Describes the style language that converts from OSM tags to Garmin types.
99 :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 ////
1022
1123 :frame: topbot
1224 :grid: rows
2020 specify -Xmx4g - note there is no space or equals sign in the option.
2121
2222 -enableassertions
23 -ea
2324 Causes an error to be thrown if an assertion written in the mkgmap code is
2425 evaluated as not true. This is useful in detecting bugs in the mkgmap code.
2526
183184 beginning and end of the important part. In combination with option
184185 --split-name-index only the words in the important part are indexed.
185186
186
187187 There are two main effects of this option:
188188 - On the PC, when zooming out, the name 'Rue de la Concorde' is only
189189 rendered as 'Concorde'.
193193 Concorde' or 'Rue du Moulin'. It may list 'Rueben Brookins Road' if
194194 that is in the map. Only MapSource shows a corresponding hint.
195195
196
197196 Another effect is that the index is smaller.
198197 See comments in the sample roadNameConfig.txt for further details.
199198
227226 like 'Planet' when you search for 'Net', it doesn't use the index because
228227 the index created by mkgmap cannot help for that search.
229228
230
231229 So, this option may help when you care about the size of the index or the
232230 memory that is needed to calculate it. The option expects a comma separated
233231 list of types or type ranges. A range is given with from-type-to-type, e.g.
251249 mkgmap:region etc. using these values.
252250
253251 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
255253 mkgmap:admin_level3 : Name of the admin_level=3 boundary
256254 ..
257255 mkgmap:admin_level11
274272 automatic assignment of address fields is somehow a best guess.
275273 is_in
276274 The is_in tag is analysed for country and region information.
277
278
279275 nearest
280276 The city/hamlet points that are closest to the element are used to
281277 assign the missing address fields. Beware that cities located in the
559555 routing. Use this option if you want address search, but do not need a map
560556 that supports routing or house number search.
561557
562
563558 --route
564559 Tells mkgmap to write NET and NOD data, which are needed in maps that
565560 support routing. If you specify this option, you do not need to specify
657652
658653 --report-dead-ends=LEVEL
659654 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.
663667
664668 --add-pois-to-lines
665669 Generate POIs for lines. For each line (must not be closed) POIs are
671675 * end - The last point of the line
672676 * inner - Each point of the line except the first and the last
673677 * mid - The middle point
678
674679 --add-pois-to-areas
675680 Generate a POI for each polygon and multipolygon. The POIs are created
676681 after the relation style but before the other styles are applied. Each POI
677682 is tagged with the same tags of the area/multipolygon. Additionally the tag
678683 mkgmap:area2poi=true is set so that it is possible to use that information
679684 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:
684687 * the first node tagged with a tag defined by the
685688 --pois-to-areas-placement option
686689 * the centre point
687 multipolygons:
688 First rule that applies of
690 multipolygons: the first rule that applies of:
689691 * the node with role=label
690692 * the centre point of the biggest area
693
691694 --pois-to-areas-placement=tag=value[;tag=value...]
692695 A POI is placed at the first node of the polygon tagged with the first
693696 tag/value pair. If none of the nodes are tagged with the first tag-value
936939
937940 Optional BITMASK (default value 3) allows you to specify which adjustments
938941 are to be made (where necessary):
939
940942 1 = increase angle between side road and outgoing main road
941943 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
767767 FontStyle=NoLabel (invisible)
768768 CustomColor=No
769769 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
805805 [end]
806806 [_polygon]
807807 type=0x3f
19551955 " !! !! !! !! !! !! !! !! "
19561956 " !! !! !! !! !! !! !! !! "
19571957 ;12345678901234567890123456789012
1958 String=River, Wadi (Intermittent)
1958 String=River/Wadi (Intermittent)
19591959 String1=0x01,Cours d’eau (intermittent)
19601960 String2=0x02,Fluß (Periodisch)
19611961 String4=0x03,Rivier (Periodiek)
57575757 type=0x065
57585758 subtype=0x0f
57595759 ;GRMN_TYPE: Geographical Named Points of Interest - Water Related/RESERVOIR/Reservoir/Non NT, NT
5760 String=Reserviour
5760 String=Reservoir
57615761 String1=0x01,Eau
57625762 String2=0x02,Wasser
57635763 String4=0x03,Water
4040 private static final short PRESERVED_MASK = 0x0002; // bit in flags is true if point should not be filtered out
4141 private static final short REPLACED_MASK = 0x0004; // bit in flags is true if point was replaced
4242 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
4444 private static final short REMOVE_MASK = 0x0020; // bit in flags is true if this point should be removed
4545 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
4646
5555 public static final int DELTA_SHIFT = HIGH_PREC_BITS - 24;
5656 private static final int MAX_DELTA = 1 << (DELTA_SHIFT - 2); // max delta abs value that is considered okay
5757 private static final long FACTOR_HP = 1L << HIGH_PREC_BITS;
58 private static final int HIGH_PREC_UNUSED_BITS = Integer.SIZE - HIGH_PREC_BITS;
5859
5960 public static final double R = 6378137.0; // Radius of earth at equator as defined by WGS84
6061 public static final double U = R * 2 * Math.PI; // circumference of earth at equator (WGS84)
224225 }
225226
226227 /**
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
229230 */
230231 public void setAddedByClipper(boolean b) {
231232 if (b)
232233 this.flags |= ADDED_BY_CLIPPER_MASK;
233234 else
234 this.flags &= ~ADDED_BY_CLIPPER_MASK;
235 this.flags &= ~ADDED_BY_CLIPPER_MASK;
235236 }
236237
237238 /**
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?
239240 * 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) {
247248 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;
251252 }
252253
253254 public boolean isToRemove() {
258259 if (b)
259260 this.flags |= REMOVE_MASK;
260261 else
261 this.flags &= ~REMOVE_MASK;
262 this.flags &= ~REMOVE_MASK;
262263 }
263264
264265 /**
515516 }
516517
517518 /**
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 /**
518529 * Calculate point on the line this->other. If d is the distance between this and other,
519530 * the point is {@code fraction * d} metres from this.
520531 * For small distances between this and other we use a flat earth approximation,
524535 public Coord makeBetweenPoint(Coord other, double fraction) {
525536 int dlatHp = other.getHighPrecLat() - getHighPrecLat();
526537 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) {
528539 // 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);
531542 return makeHighPrecCoord(latHighPrec, lonHighPrec);
532543 }
533544 double brng = this.bearingToOnRhumbLine(other, true);
534545 double dist = this.distance(other) * fraction;
535 return this.destOnRhumLine(dist, brng);
546 return this.destOnRhumbLine(dist, brng);
536547 }
537548
538549
762773 * @param brng bearing in degrees
763774 * @return a new Coord instance
764775 */
765 public Coord destOnRhumLine(double dist, double brng){
776 public Coord destOnRhumbLine(double dist, double brng){
766777 double distRad = dist / R; // angular distance in radians
767778 double lat1 = hpToRadians(this.getHighPrecLat());
768779 double lon1 = hpToRadians(this.getHighPrecLon());
812823 // simple calculation using Herons formula will fail
813824 // calculate x, the point on line a-b which is as far away from a as this point
814825 double b_ab = a.bearingToOnRhumbLine(b, true);
815 Coord x = a.destOnRhumLine(ap, b_ab);
826 Coord x = a.destOnRhumbLine(ap, b_ab);
816827 // this dist between these two points is not exactly
817828 // the perpendicul distance, but close enough
818829 dist = x.distance(this);
879890 double newLon = lon + Math.atan2(Math.sin(bearing) * Math.sin(angularDistance) * Math.cos(lat), Math.cos(angularDistance) - Math.sin(lat) * Math.sin(newLat));
880891 return new Coord(Math.toDegrees(newLat), Math.toDegrees(newLon));
881892 }
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 }
883906 }
136136 FileChannel chan = FileChannel.open(Paths.get(name), StandardOpenOption.READ);
137137 return openFs(name, chan);
138138 } catch (IOException e) {
139 throw new FileNotFoundException("Failed to create or open file");
139 throw new FileNotFoundException("Failed to create or open file " + name);
140140 }
141141 }
142142
186186 public boolean containsExpression(String exp) {
187187 return expression.toString().contains(exp);
188188 }
189
190 @Override
191 public void augmentWith(uk.me.parabola.mkgmap.reader.osm.ElementSaver elementSaver) {
192 expression.augmentWith(elementSaver);
193 }
194
189195 }
2121 import java.io.FileNotFoundException;
2222 import java.io.InputStreamReader;
2323 import java.io.Reader;
24 import java.io.UnsupportedEncodingException;
2524 import java.nio.charset.StandardCharsets;
2625 import java.util.ArrayList;
2726 import java.util.List;
3737 import uk.me.parabola.mkgmap.osmstyle.eval.OrOp;
3838 import uk.me.parabola.mkgmap.osmstyle.eval.RegexOp;
3939 import uk.me.parabola.mkgmap.osmstyle.eval.ValueOp;
40 import uk.me.parabola.mkgmap.osmstyle.function.StyleFunction;
4041 import uk.me.parabola.mkgmap.scan.SyntaxException;
4142 import uk.me.parabola.mkgmap.scan.TokenScanner;
4243
6768 Logger log = Logger.getLogger(getClass());
6869
6970 public Op arrange(Op expr) {
70 log.debug("IN: " + fmtExpr(expr));
71 if (log.isDebugEnabled())
72 log.debug("IN:", fmtExpr(expr));
7173 Op op = arrangeTop(expr);
72 log.debug("OUT: " + fmtExpr(expr));
74 if (log.isDebugEnabled())
75 log.debug("OUT:", fmtExpr(expr));
7376 return op;
7477 }
7578
375378 private int selectivity(Op op) {
376379 // Operations that involve a non-indexable function must always go to the back.
377380 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();
380384 }
381385
382386 switch (op.getType()) {
425429 Op second = op.getSecond();
426430
427431 String keystring = null;
432 String valuestring = null;
428433 if (op.isType(EQUALS) && first.isType(FUNCTION) && second.isType(VALUE)) {
429 keystring = first.getKeyValue() + "=" + second.getKeyValue();
434 keystring = first.getKeyValue();
435 valuestring = second.getKeyValue();
430436 } else if (op.isType(EXISTS)) {
431 keystring = first.getKeyValue() + "=*";
437 keystring = first.getKeyValue();
438 valuestring = "*";
432439 } 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();
435443 } 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 }
439448 } else if (first.isType(NOT_EXISTS)) {
440 throw new SyntaxException(scanner, "Cannot start rule with tag!=*");
449 // invalid rule
441450 }
442451 }
443452
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;
448457 }
449458
450459 /**
115115 public boolean containsExpression(String exp) {
116116 return expression.toString().contains(exp);
117117 }
118
119 @Override
120 public void augmentWith(uk.me.parabola.mkgmap.reader.osm.ElementSaver elementSaver) {
121 expression.augmentWith(elementSaver);
122 }
123
118124 }
2121 import java.io.InputStream;
2222 import java.io.InputStreamReader;
2323 import java.io.Reader;
24 import java.io.UnsupportedEncodingException;
25 import java.nio.charset.StandardCharsets;
2624 import java.net.JarURLConnection;
2725 import java.net.MalformedURLException;
2826 import java.net.URL;
27 import java.nio.charset.StandardCharsets;
2928 import java.util.ArrayList;
3029 import java.util.Enumeration;
3130 import java.util.List;
311311 return candidates;
312312 }
313313
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 }
314321 }
3838 import java.util.regex.Pattern;
3939
4040 import uk.me.parabola.imgfmt.ExitException;
41 import uk.me.parabola.imgfmt.Utils;
4241 import uk.me.parabola.log.Logger;
4342 import uk.me.parabola.mkgmap.Options;
4443 import uk.me.parabola.mkgmap.general.LevelInfo;
104103 private OverlayReader overlays;
105104 private final boolean performChecks;
106105
106 private Collection<String> deadEndTags = new ArrayList<>();
107107
108108 /**
109109 * Create a style from the given location and name.
129129 * include the version file being missing.
130130 */
131131 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
132140 location = loc;
133141 fileLoader = StyleFileLoader.createStyleLoader(loc, name);
134142 this.performChecks = performChecks;
211219 set.addAll(lines.getUsedTags());
212220 set.addAll(polygons.getUsedTags());
213221 set.addAll(nodes.getUsedTags());
214
222 set.addAll(deadEndTags);
223
215224 // this is to allow style authors to say that tags are really used even
216225 // if they are not found in the style file. This is mostly to work
217226 // around situations that we haven't thought of - the style is expected
159159 private final boolean keepBlanks;
160160
161161 private LineAdder lineAdder;
162
162
163163 public StyledConverter(Style style, MapCollector collector, EnhancedProperties props) {
164164 this.collector = collector;
165165
310310
311311 if (way.tagIsLikeYes(onewayTagKey)) {
312312 way.addTag(onewayTagKey, "yes");
313 if (foundType.isRoad() && checkFixmeCoords(way) )
313 if (foundType.isRoad() && hasSkipDeadEndCheckNode(way))
314314 way.addTag("mkgmap:dead-end-check", "false");
315315 } else {
316316 way.deleteTag(onewayTagKey);
387387 }
388388
389389 /**
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
391391 * @param way the way to check
392 * @return true if fixme flag was found
392 * @return true if flag was found
393393 */
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();
396396 }
397397
398398
695695 }
696696
697697 /**
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 /**
698712 * Built in rules to run after converting the element.
699713 */
700714 private static void postConvertRules(Element el, GType type) {
735749 for (RestrictionRelation rr : rrList) {
736750 if (!rr.isValidWithoutWay(way.getId())) {
737751 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",
739753 way.toBrowseURL(), reason);
740754 }
741755 rr.setInvalid();
966980 log.info("Found", numRoads, "roads",
967981 numDriveOnLeftRoads, "in drive-on-left country,",
968982 numDriveOnRightRoads, "in drive-on-right country, and",
969 numDriveOnSideUnknown, " with unknwon country");
983 numDriveOnSideUnknown, " with unknown country");
970984 if (numDriveOnLeftRoads> 0 && numDriveOnRightRoads > 0)
971985 log.error("Attention: Tile contains both drive-on-left (" + numDriveOnLeftRoads +
972986 ") and drive-on-right roads (" + numDriveOnRightRoads + ")");
6565
6666 return sb.toString();
6767 }
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
6875 }
216216 }
217217
218218 }
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
219226 }
1414
1515 import java.util.Collection;
1616 import java.util.Collections;
17 import java.util.ArrayList;
18 import java.util.List;
1719 import java.util.HashSet;
1820 import java.util.Set;
1921 import java.util.Stack;
2628 import uk.me.parabola.mkgmap.scan.SyntaxException;
2729 import uk.me.parabola.mkgmap.scan.TokenScanner;
2830 import uk.me.parabola.mkgmap.scan.WordInfo;
31 import uk.me.parabola.mkgmap.scan.Token;
32 import uk.me.parabola.mkgmap.scan.TokType;
2933
3034 import static uk.me.parabola.mkgmap.osmstyle.eval.NodeType.*;
3135
3640 public class ExpressionReader {
3741 private static final Logger log = Logger.getLogger(ExpressionReader.class);
3842
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<>();
4145 private final TokenScanner scanner;
4246 private final FeatureKind kind;
4347
44 private final Set<String> usedTags = new HashSet<String>();
48 private final Set<String> usedTags = new HashSet<>();
4549
4650 public ExpressionReader(TokenScanner scanner, FeatureKind kind) {
4751 this.scanner = scanner;
7680 pushValue(wordInfo.getText());
7781 } else if (wordInfo.getText().charAt(0) == '$') {
7882 String tagname = scanner.nextWord();
79 if (tagname.equals("{")) {
83 if ("{".equals(tagname)) {
8084 tagname = scanner.nextWord();
8185 scanner.validateNext("}");
8286 }
8589 // it is a function
8690 // this requires a () after the function name
8791 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);
88108 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 }
90114 } else {
91115 pushValue(wordInfo.getText());
92116 }
122146 * @param ifStack
123147 * @return
124148 */
125 private Op appendIfExpr(Op expr, Collection<Op[]> ifStack) {
149 private static Op appendIfExpr(Op expr, Collection<Op[]> ifStack) {
126150 Op result = expr;
127151 for (Op[] ops : ifStack) {
128152 if (result != null) {
130154 and.setFirst(result);
131155 and.setSecond(ops[0].copy());
132156 result = and;
133 } else
157 } else {
134158 result = ops[0].copy();
135
159 }
136160 }
137161 return result;
138162 }
142166 * @param token The string to test.
143167 * @return True if this looks like an operator.
144168 */
145 private boolean isOperation(WordInfo token) {
169 private static boolean isOperation(WordInfo token) {
146170 // A quoted word is not an operator eg: '=' is a string.
147171 if (token.isQuoted())
148172 return false;
173197 */
174198 private void saveOp(String value) {
175199 log.debug("save op", value);
176 if (value.equals("#")) {
200 if ("#".equals(value)) {
177201 scanner.skipLine();
178202 return;
179203 }
252276 op.setFirst(arg1);
253277 }
254278 } else if (!op.isType(OPEN_PAREN)) {
255 if (stack.size() < 1)
279 if (stack.isEmpty())
256280 throw new SyntaxException(scanner, String.format("Missing argument for %s operator",
257281 op.getType().toSymbol()));
258282 op.setFirst(stack.pop());
276300 *
277301 * @param functionName A name to look up.
278302 */
279 private void saveFunction(String functionName) {
303 private void saveFunction(String functionName, List<String> functionParams) {
280304 StyleFunction function = FunctionFactory.createFunction(functionName);
281305 if (function == null)
282306 throw new SyntaxException(String.format("No function with name '%s()'", functionName));
307 function.setParams(functionParams, kind);
283308
284309 // TODO: supportsWay split into supportsPoly{line,gon}, or one function supports(kind)
285310 boolean supported = false;
303328
304329 if (!supported)
305330 throw new SyntaxException(String.format("Function '%s()' not supported for %s", functionName, kind));
306
331 usedTags.addAll(function.getUsedTags());
307332 stack.push(function);
308333 }
309334
168168 public Set<String> getEvaluatedTagKeys() {
169169 return wrapped.getEvaluatedTagKeys();
170170 }
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
171180 }
3434 * @param el The OSM element to be tested.
3535 * @return True if the expression is true for the given element.
3636 */
37 public boolean eval(Element el);
37 boolean eval(Element el);
3838
3939 /**
4040 * Evaluate the expression using a cache.
4242 * @param el The OSM element to be tested.
4343 * @return True if the expression is true for the given element.
4444 */
45 public boolean eval(int cacheId, Element el);
45 boolean eval(int cacheId, Element el);
4646
4747
4848 /**
4949 * Does this operation have a higher priority that the other one?
5050 * @param other The other operation.
5151 */
52 public boolean hasHigherPriority(Op other);
52 boolean hasHigherPriority(Op other);
5353
5454 /**
5555 * Get the first operand.
5656 */
57 public Op getFirst();
57 Op getFirst();
5858
5959 /**
6060 * Set the first operand.
6161 */
62 public <T extends Op> T setFirst(Op first);
62 <T extends Op> T setFirst(Op first);
6363
6464 /**
6565 * Set the second operand.
6666 * Only supported on binary nodes, but declared here to avoid casts all over the place.
6767 */
68 public default void setSecond(Op second) {
68 default void setSecond(Op second) {
6969 throw new UnsupportedOperationException("setSecond only supported on binary nodes");
7070 }
7171
7474 * then null is returned.
7575 * @return The right hand side, or null if there is not one.
7676 */
77 public Op getSecond();
77 Op getSecond();
7878
7979 /**
8080 * Set both first and second in one call.
8181 *
8282 * Only supported on BinaryOp types.
8383 */
84 public <T extends Op> T set(Op first, Op second);
84 <T extends Op> T set(Op first, Op second);
8585
8686 /** Get the operation type */
87 public NodeType getType();
87 NodeType getType();
8888
8989 /**
9090 * For operations that are value types this is the string value.
9494 *
9595 * @return The value, or UnsupportedOperationException if it does not have a value.
9696 */
97 public String value(Element el);
97 String value(Element el);
9898
9999 /**
100100 * For a value-type node, this is a key value associated with value. For a base Value node
101101 * this is the same as value(), but if value() is overridden then it may not be.
102102 */
103 public String getKeyValue();
103 String getKeyValue();
104104
105105 /**
106106 * Test the node type and return true if it matches the given argument.
107107 */
108 public boolean isType(NodeType value);
108 boolean isType(NodeType value);
109109
110110 /**
111111 * For an operation this is a number that determines the precedence of this operation.
112112 * Used when building the node tree. Higher numbers bind more tightly.
113113 */
114 public int priority();
114 int priority();
115115
116116 /**
117117 * @return a set with the tag keys which are evaluated, maybe empty
118118 */
119 public Set<String> getEvaluatedTagKeys();
119 Set<String> getEvaluatedTagKeys();
120120
121121
122122 /**
126126 * they are never modified by the arranger code. If you are using this for something else
127127 * you may need a different method.
128128 */
129 public default Op copy() {
129 default Op copy() {
130130 NodeType t = getType();
131131 if (t == AND || t == OR) {
132132 return AbstractOp.createOp(getType())
134134 } else
135135 return this;
136136 }
137
138 default void augmentWith(uk.me.parabola.mkgmap.reader.osm.ElementSaver elementSaver) {}
139
137140 }
33
44 import uk.me.parabola.imgfmt.app.Coord;
55 import uk.me.parabola.mkgmap.reader.osm.Element;
6 import uk.me.parabola.mkgmap.reader.osm.MultiPolygonRelation;
76 import uk.me.parabola.mkgmap.reader.osm.Way;
87
98 /**
2120 */
2221 public class AreaSizeFunction extends CachedFunction {
2322
24 private final boolean orderByDecreasingArea = true;
25
2623 public AreaSizeFunction() {
2724 super(null);
2825 }
3128 if (el instanceof Way) {
3229 Way w = (Way)el;
3330 // a non closed way has size 0
34 if (w.hasEqualEndPoints() == false) {
31 if (!w.hasEqualEndPoints()) {
3532 return "0";
3633 }
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));
4840 }
4941 return null;
5042 }
5143
44 @Override
5245 public String getName() {
5346 return "area_size";
5447 }
5548
49 @Override
5650 public boolean supportsWay() {
5751 return true;
5852 }
5953
54 @Override
55 public int getComplexity() {
56 return 2;
57 }
6058 }
4747 return new TypeFunction();
4848 if ("osmid".equals(name))
4949 return new OsmIdFunction();
50 if ("is_in".equals(name))
51 return new IsInFunction();
5052
5153 return null;
5254 }
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 }
7979 public boolean supportsRelation() {
8080 return true;
8181 }
82
83 @Override
84 public int getComplexity() {
85 return 2;
86 }
8287 }
1414
1515 import java.text.DecimalFormat;
1616 import java.text.DecimalFormatSymbols;
17 import java.util.Collections;
1718 import java.util.Locale;
19 import java.util.Set;
1820 import java.util.regex.Pattern;
1921
2022 import uk.me.parabola.mkgmap.reader.osm.Element;
9597
9698 }
9799
100 @Override
98101 public String getName() {
99102 switch (this.unit) {
100103 case MPH:
105108 }
106109 }
107110
111 @Override
108112 public boolean supportsWay() {
109113 return true;
110114 }
115
116 @Override
117 public Set<String> getUsedTags() {
118 return Collections.singleton("maxspeed");
119 }
111120 }
1212
1313 package uk.me.parabola.mkgmap.osmstyle.function;
1414
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
1522 import uk.me.parabola.mkgmap.osmstyle.eval.ValueOp;
23 import uk.me.parabola.mkgmap.reader.osm.FeatureKind;
1624 import uk.me.parabola.mkgmap.reader.osm.Node;
1725 import uk.me.parabola.mkgmap.reader.osm.Relation;
1826 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;
2128
2229 /**
2330 * The interface for all functions that can be used within a style file.<br>
2734 */
2835 public abstract class StyleFunction extends ValueOp {
2936
37 protected int reqdNumParams = 0;
38 protected List<String> params;
39 protected FeatureKind kind;
40
3041 public StyleFunction(String value) {
3142 super(value);
3243 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;
3351 }
3452
3553 /**
6987 return getKeyValue();
7088 }
7189
90 @Override
7291 public String toString() {
7392 return getName() + "()";
7493 }
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 }
75109 }
401401 Coord c = c1.makeBetweenPoint(c2, fraction);
402402 interpolated.add(c);
403403 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)));
406404 return interpolated;
407405 }
408406 rest = 0;
1515 */
1616 package uk.me.parabola.mkgmap.reader;
1717
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;
1825 import java.util.List;
26 import java.util.function.UnaryOperator;
1927
28 import uk.me.parabola.imgfmt.ExitException;
2029 import uk.me.parabola.imgfmt.app.Area;
2130 import uk.me.parabola.imgfmt.app.net.RoadNetwork;
2231 import uk.me.parabola.imgfmt.app.trergn.Overview;
32 import uk.me.parabola.mkgmap.Version;
2333 import uk.me.parabola.mkgmap.general.MapDataSource;
2434 import uk.me.parabola.mkgmap.general.MapDetails;
2535 import uk.me.parabola.mkgmap.general.MapLine;
3848 protected final MapDetails mapper = new MapDetails();
3949 private EnhancedProperties configProps;
4050 private boolean driveOnLeft;
51 private static final LocalDateTime now = LocalDateTime.now();
4152
4253 /**
4354 * Get the area that this map covers. Delegates to the map collector.
131142 driveOnLeft = b;
132143 }
133144
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
134173 }
6565
6666 // Options
6767 private final boolean ignoreTurnRestrictions;
68 private final String[] deadEndArgs;
6869
6970 /** name of the tag that contains a ;-separated list of tag names that should be removed after all elements have been processed */
7071 public static final short MKGMAP_REMOVE_TAG_KEY = TagDict.getInstance().xlate("mkgmap:removetags");
8182 }
8283
8384 ignoreTurnRestrictions = args.getProperty("ignore-turn-restrictions", false) || !args.containsKey("route");
85 deadEndArgs = args.getProperty("dead-ends", "fixme,FIXME").split(",");
8486 }
8587
8688 /**
219221 makeBoundaryNodes();
220222
221223 converter.setBoundingBox(getBoundingBox());
224 converter.augmentWith(this);
225
222226
223227 for (Relation r : relationMap.values())
224228 converter.convertRelation(r);
225229
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()) {
229231 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 }
232243 }
233244 }
234245
5353 */
5454 public void convertRelation(Relation relation);
5555
56 public default void augmentWith(uk.me.parabola.mkgmap.reader.osm.ElementSaver elementSaver) {
57 }
58
5659 /**
5760 * Set the bounding box for this map. This should be set before any other
5861 * elements are converted if you want to use it.
1616 package uk.me.parabola.mkgmap.reader.osm;
1717
1818 import java.io.BufferedReader;
19 import java.io.File;
19 import java.io.FileInputStream;
2020 import java.io.FileNotFoundException;
21 import java.io.InputStreamReader;
22 import java.io.FileInputStream;
2321 import java.io.IOException;
2422 import java.io.InputStream;
23 import java.io.InputStreamReader;
2524 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;
3025 import java.util.ArrayList;
3126 import java.util.HashMap;
3227 import java.util.HashSet;
3328 import java.util.List;
3429 import java.util.Map;
3530 import java.util.Set;
36 import java.util.function.UnaryOperator;
37
38 import uk.me.parabola.imgfmt.ExitException;
31
3932 import uk.me.parabola.imgfmt.Utils;
4033 import uk.me.parabola.log.Logger;
41 import uk.me.parabola.mkgmap.Version;
4234 import uk.me.parabola.mkgmap.general.LevelInfo;
4335 import uk.me.parabola.mkgmap.general.LoadableMapDataSource;
4436 import uk.me.parabola.mkgmap.osmstyle.NameFinder;
7971 private final Set<String> usedTags = new HashSet<>();
8072 protected ElementSaver elementSaver;
8173 protected OsmReadingHooks osmReadingHooks;
82 private static final LocalDateTime now = LocalDateTime.now();
8374
8475 protected static final List<OsmHandler> handlers;
8576 static {
192183 public String[] copyrightMessages() {
193184 String copyrightFileName = getConfig().getProperty("copyright-file", null);
194185 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);
215187 }
216188 String note = getConfig().getProperty("copyright-message",
217189 "OpenStreetMap.org contributors. See: http://wiki.openstreetmap.org/index.php/Attribution");
264236 protected OsmReadingHooks pluginChain(ElementSaver saver, EnhancedProperties props) {
265237 List<OsmReadingHooks> plugins = new ArrayList<>();
266238 for (OsmReadingHooks p : getPossibleHooks()) {
239 if (p instanceof ResidentialHook && style != null && !style.getUsedTags().contains("mkgmap:residential"))
240 continue;
267241 if (p.init(saver, props)){
268242 plugins.add(p);
269243 if (p instanceof RelationStyleHook)
3030 * @param el The element as read from an OSM xml file in 'tag' format.
3131 * @param result The resolved Garmin type that will go into the map.
3232 */
33 public void resolveType(Element el, TypeResult result);
33 void resolveType(Element el, TypeResult result);
3434
3535 /**
3636 *
4242 * @param result The resolved Garmin type that will go into the map.
4343 * @return
4444 */
45 public int resolveType(int cacheId, Element el, TypeResult result);
45 int resolveType(int cacheId, Element el, TypeResult result);
4646
4747 /**
4848 * Sets the finalize rules that are executed when
5050 *
5151 * @param finalizeRule finalize rule(s)
5252 */
53 public void setFinalizeRule(Rule finalizeRule);
53 void setFinalizeRule(Rule finalizeRule);
5454
55 public void printStats(String header);
55 void printStats(String header);
5656
57 public Rule getFinalizeRule();
57 Rule getFinalizeRule();
5858
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
6163 }
2727 import java.util.LinkedList;
2828 import java.util.List;
2929 import java.util.Map;
30 import java.util.Map.Entry;
3130 import java.util.NavigableMap;
3231 import java.util.NavigableSet;
3332 import java.util.Set;
7473 private ElementSaver saver;
7574
7675 private List<Way> shoreline = new ArrayList<>();
76 private List<Way> islands = new ArrayList<>();
77 private List<Way> antiIslands = new ArrayList<>();
78 private Area tileBounds;
7779 private boolean generateSeaBackground = true;
7880
7981 private String[] coastlineFilenames;
425427 if (precompSea != null)
426428 splitCoastLineToLineAndShape(way, natural);
427429 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 */
428443 // create copy of way that has only the natural=coastline tag
429444 Way shore = new Way(way.getOriginalId(), way.getPoints());
430445 shore.setFakeId();
582597 } else {
583598 // using polygons
584599 // first add the complete bounding box as sea
585 saver.addWay(createSeaWay(saver.getBoundingBox(), false));
600 saver.addWay(createSeaWay(false));
586601 }
587602
588603 // check if the land tags need to be changed
792807 */
793808 @Override
794809 public void end() {
810 tileBounds = saver.getBoundingBox();
795811 // precompiled sea has highest priority
796812 // if it is set do not perform any other algorithm
797813 if (precompSea != null && precompIndex.get() != null) {
799815 return;
800816 }
801817
802 final Area tileBounds = saver.getBoundingBox();
803818 if (coastlineFilenames == null) {
804819 log.info("Shorelines before join", shoreline.size());
805820 shoreline = joinWays(shoreline);
815830 }
816831
817832 // clip all shoreline segments
818 clipShorlineSegments(shoreline, tileBounds);
833 clipShorlineSegments();
819834
820835 if(shoreline.isEmpty()) {
821836 // No sea required
825840 // some sea
826841 // No matter if the multipolygon option is used it is
827842 // only necessary to create a land polygon
828 saver.addWay(createLandWay(tileBounds));
843 saver.addWay(createLandWay());
829844 // nothing more to do
830845 return;
831846 }
832847
833 long multiId = FakeIdGenerator.makeFakeId();
834848 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 }
841849
842
843 List<Way> islands = new ArrayList<>();
844
845850 // 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 }
847860
848861 // the remaining shoreline segments should intersect the boundary
849862 // 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);
863865
864866 if (generateSeaBackground) {
865867 // the background is sea so all anti-islands should be
866868 // 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);
870882
871883 log.info("sea: ", sea);
872884 saver.addWay(sea);
873 if(seaRelation != null) {
885 if(seaRelation != null)
874886 seaRelation.addElement("outer", sea);
875 }
876887 } else {
877888 // background is land
878
889 createSeaPolygons(hitMap);
890 processAntiIslands(false);
891
879892 // generate a land polygon so that the tile's
880893 // background colour will match the land colour on the
881894 // tiles that do contain some sea
882 Way land = createLandWay(tileBounds);
895 Way land = createLandWay();
883896 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) {
890901 SeaPolygonRelation coastRel = saver.createSeaPolyRelation(seaRelation);
891902 coastRel.setFloodBlocker(floodblocker);
892903 if (floodblocker) {
901912 }
902913
903914 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) {
907939 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() {
932957 long landId = FakeIdGenerator.makeFakeId();
933958 Way land = new Way(landId, tileBounds.toCoords());
934959 land.addTag(landTag[0], landTag[1]);
937962
938963 /**
939964 * Create a sea polygon from the given tile bounds
940 * @param tileBounds
941965 * @param enlarge if true, make sure that the polygon is slightly larger than the tile bounds
942966 * @return the created way
943967 */
944 private static Way createSeaWay(Area tileBounds, boolean enlarge) {
968 private Way createSeaWay(boolean enlarge) {
945969 log.info("generating sea, seaBounds=", tileBounds);
946970 Area bbox = tileBounds;
947971 long seaId = FakeIdGenerator.makeFakeId();
954978 bbox = new Area(bbox.getMinLat() - 1, bbox.getMinLong() - 1, bbox.getMaxLat() + 1, bbox.getMaxLong() + 1);
955979 }
956980 Way sea = new Way(seaId, bbox.toCoords());
981 sea.reverse(); // make clockwise for consistency
957982 sea.addTag("natural", "sea");
958983 sea.setFullArea(SEA_SIZE);
959984 return sea;
964989 * @param shoreline All the the ways making up the coast.
965990 * @param bounds The map bounds.
966991 */
967 private static void clipShorlineSegments(List<Way> shoreline, Area bounds) {
992 private void clipShorlineSegments() {
968993 List<Way> toBeRemoved = new ArrayList<>();
969994 List<Way> toBeAdded = new ArrayList<>();
970995 for (Way segment : shoreline) {
971996 List<Coord> points = segment.getPoints();
972 List<List<Coord>> clipped = LineClipper.clip(bounds, points);
997 List<List<Coord>> clipped = LineClipper.clip(tileBounds, points);
973998 if (clipped != null) {
974999 log.info("clipping", segment);
9751000 toBeRemoved.add(segment);
9871012 }
9881013
9891014 /**
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() {
9981019 Iterator<Way> it = shoreline.iterator();
9991020 while (it.hasNext()) {
10001021 Way w = it.next();
10011022 if (w.hasIdenticalEndPoints()) {
1002 log.info("adding island", w);
1003 islands.add(w);
1023 addClosedShore(w);
10041024 it.remove();
10051025 }
10061026 }
10071027
1008 closeGaps(shoreline, tileBounds);
1028 closeGaps();
10091029 // there may be more islands now
10101030 it = shoreline.iterator();
10111031 while (it.hasNext()) {
10121032 Way w = it.next();
10131033 if (w.hasIdenticalEndPoints()) {
1014 log.debug("island after concatenating");
1015 islands.add(w);
1034 log.debug("closed after concatenating", w);
1035 addClosedShore(w);
10161036 it.remove();
10171037 }
10181038 }
10191039 }
10201040
1021 private void closeGaps(List<Way> shoreline, Area bounds) {
1041 private void closeGaps() {
10221042 if (maxCoastlineGap <= 0)
10231043 return;
10241044
10331053 if (w1.hasIdenticalEndPoints())
10341054 continue;
10351055 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);
10381058 if (closed != null) {
10391059 saver.addWay(closed);
10401060 changed = true;
10441064 } while (changed);
10451065 }
10461066
1047 private Way tryCloseGap(List<Way> shoreline, Way w1, Area bounds) {
1067 private Way tryCloseGap(Way w1) {
10481068 Coord w1e = w1.getLastPoint();
10491069 Way nearest = null;
10501070 double smallestGap = Double.MAX_VALUE;
10521072 if (w1 == w2 || w2.hasIdenticalEndPoints())
10531073 continue;
10541074 Coord w2s = w2.getFirstPoint();
1055 if (!bounds.onBoundary(w2s)) {
1075 if (!tileBounds.onBoundary(w2s)) {
10561076 double gap = w1e.distance(w2s);
10571077 if (gap < smallestGap) {
10581078 nearest = w2;
10871107 return null;
10881108 }
10891109
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
10901132 /**
10911133 * Add lines to ways that touch or cross the sea bounds so that the way is closed along the edges of the bounds.
10921134 * 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
10951135 * @param hitMap A map of the 'hits' where the shore line intersects the boundary.
10961136 */
1097 private void createLandPolygons(Area tileBounds, List<Way> islands, NavigableMap<Double, Way> hitMap) {
1137 private void createLandPolygons(NavigableMap<Double, Way> hitMap) {
10981138 NavigableSet<Double> hits = hitMap.navigableKeySet();
10991139 while (!hits.isEmpty()) {
11001140 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;
11051144 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 }
12071232 }
12081233
12091234 /**
12101235 * Find the points where the remaining shore line segments intersect with the
12111236 * 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.
12161237 * @return A map of the 'hits' where the shore line intersects the boundary.
12171238 */
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() {
12221240 NavigableMap<Double, Way> hitMap = new TreeMap<>();
12231241 for (Way w : shoreline) {
12241242 Coord pStart = w.getFirstPoint();
12301248 // nice case: both ends touch the boundary
12311249 log.debug("hits: ", hStart, hEnd);
12321250 hitMap.put(hStart, w);
1233 hitMap.put(hEnd, null);
1251 hitMap.put(hEnd, null); // put this for verifyHits which then deletes it
12341252 } else {
1235 /*
1253 /*
12361254 * 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.
12451269 */
12461270 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);
12551285 }
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;
12601292 }
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);
12781296 } else if (extendSeaSectors) {
1279 // create additional points at next border to prevent triangles from point 2
1297 // join to nearest tile border
12801298 if (null == hStart) {
12811299 hStart = getNextEdgeHit(tileBounds, pStart);
12821300 w.getPoints().add(0, getPoint(tileBounds, hStart));
12871305 }
12881306 log.debug("hits (second try): ", hStart, hEnd);
12891307 hitMap.put(hStart, w);
1290 hitMap.put(hEnd, null);
1308 hitMap.put(hEnd, null); // put this for verifyHits which then deletes it
12911309 } else {
12921310 // show the coastline even though we can't produce
12931311 // a polygon for the land
12941312 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");
12981314 saver.addWay(w);
12991315 }
13001316 }
13011317 }
13021318 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 }
13031346 }
13041347
13051348 // create the point where the shoreline hits the sea bounds
13331376 plonHp = aMinLongHP;
13341377 break;
13351378 default:
1336 throw new MapFailedException("illegal state");
1379 throw new MapFailedException("GetPoint edge: " + edgePos);
13371380 }
13381381 return Coord.makeHighPrecCoord(platHp, plonHp);
13391382 }
162162 * The returned Tags must not be modified by the caller.
163163 */
164164 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 }
181193 }
182194 return res;
183195 }
238250 return root.getCoveredArea(admLevel, "_");
239251 }
240252
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
252253 /**
253254 * Save the BoundaryQuadTree to an open stream. The format is QUADTREE_DATA_FORMAT.
254255 * @param stream
325326 try {
326327 while (true) {
327328 String type = inpStream.readUTF();
328 if (type.equals("TAGS")){
329 if ("TAGS".equals(type)){
329330 String id = inpStream.readUTF();
330331 Tags tags = new Tags();
331332 int noOfTags = inpStream.readInt();
336337 }
337338 boundaryTags.put(id, tags);
338339 }
339 else if (type.equals("AREA")){
340 else if ("AREA".equals(type)){
340341 if (isFirstArea){
341342 isFirstArea = false;
342343 prepareLocationInfo();
457458 }
458459
459460 /**
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 /**
492461 * Return location relevant Tags for the point defined by Coord
493462 * @param co the point
494463 * @return a reference to the internal Tags or null if the point was not found.
497466 private Tags get(Coord co/*, String treePath*/){
498467 if (!this.bounds.contains(co))
499468 return null;
500 if (isLeaf){
469 if (isLeaf) {
501470 if (nodes == null || nodes.isEmpty())
502471 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);
505474 for (NodeElem nodeElem : nodes) {
506475 if (nodeElem.tagMask > 0 && nodeElem.getArea().contains(lon, lat)) {
507476 return nodeElem.locTags;
508477 }
509478 }
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;
516484 }
517485 }
518486 return null;
754722 }
755723 long t1 = System.currentTimeMillis();
756724 if (DEBUG){
757 if (treePath.equals(DEBUG_TREEPATH) || DEBUG_TREEPATH.equals("all")){
725 if (treePath.equals(DEBUG_TREEPATH) || "all".equals(DEBUG_TREEPATH)) {
758726 for (NodeElem nodeElem: nodes){
759727 nodeElem.saveGPX("start",treePath);
760728 }
772740 for (int i=0; i < nodes.size(); i++){
773741 NodeElem toAdd = nodes.get(i);
774742 if (DEBUG) {
775 if (treePath.equals(DEBUG_TREEPATH) || DEBUG_TREEPATH.equals("all")) {
743 if (treePath.equals(DEBUG_TREEPATH) || "all".equals(DEBUG_TREEPATH)) {
776744 for (NodeElem nodeElem : reworked) {
777745 nodeElem.saveGPX("debug" + i, treePath);
778746 }
103103 private int endLevel;
104104 private char elevUnits;
105105 private int currentLevel;
106 private boolean dataHighLevel;
107 private boolean background;
106108 private int poiDispFlag;
107109 private String defaultCountry;
108110 private String defaultRegion;
187189 }
188190
189191 /**
190 * Get the copyright message. We use whatever was specified inside the
191 * MPF itself.
192 * Get the copyright message.
192193 *
193194 * @return A string description of the copyright.
194195 */
195196 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 };
197205 }
198206
199207 /**
209217
210218 extraAttributes = null;
211219
212 if (name.equalsIgnoreCase("IMG ID")) {
220 if ("IMG ID".equalsIgnoreCase(name)) {
213221 section = S_IMG_ID;
214222 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)) {
216224 point = new MapPoint();
217225 section = S_POINT;
218 } else if (name.equalsIgnoreCase("POLYLINE") || name.equals("RGN40")) {
226 } else if ("POLYLINE".equalsIgnoreCase(name) || "RGN40".equals(name)) {
219227 polyline = new MapLine();
220228 roadHelper.clear();
221229 section = S_POLYLINE;
222 } else if (name.equalsIgnoreCase("POLYGON") || name.equals("RGN80")) {
230 } else if ("POLYGON".equalsIgnoreCase(name) || "RGN80".equals(name)) {
223231 shape = new MapShape();
224232 section = S_POLYGON;
225 } else if (name.equalsIgnoreCase("Restrict")) {
233 } else if ("Restrict".equalsIgnoreCase(name)) {
226234 restriction = new PolishTurnRestriction();
227235 section = S_RESTRICTION;
228236 }
305313 if (!lineStringMap.isEmpty()) {
306314 if (extraAttributes != null && shape.hasExtendedType())
307315 shape.setExtTypeAttributes(makeExtTypeAttributes());
316 if (background && !dataHighLevel)
317 endLevel = levels.length -1;
308318 for (Map.Entry<Integer , List<List<Coord>>> entry : lineStringMap.entrySet()) {
309319 setResolution(shape, entry.getKey());
310320 addShapesFromPattern(entry.getValue());
328338 endLevel = 0;
329339 lineStringMap.clear();
330340 currentLevel = 0;
341 dataHighLevel = false;
342 background = false;
331343 }
332344
333345 private void addShapesFromPattern(List<List<Coord>> pointsLists) {
414426 * @param value Its value.
415427 */
416428 private void point(String name, String value) {
417 if (name.equals("Type")) {
429 if ("Type".equals(name)) {
418430 int type = Integer.decode(value);
419431 if (type <= 0xff)
420432 type <<= 8;
421433 point.setType(type);
422434 checkType(FeatureKind.POINT, point.getType());
423 } else if (name.equals("SubType")) {
435 } else if ("SubType".equals(name)) {
424436 int subtype = Integer.decode(value);
425437 int type = point.getType();
426438 point.setType(type | subtype);
447459 * @see #point
448460 */
449461 private void line(String name, String value) {
450 if (name.equals("Type")) {
462 if ("Type".equals(name)) {
451463 polyline.setType(Integer.decode(value));
452464 checkType(FeatureKind.POLYLINE, polyline.getType());
453465 } else if (name.startsWith("Data")) {
459471 (polyline.getType() == 0x22)) {
460472 fixElevation();
461473 }
462 } else if (name.equals("RoadID")) {
474 } else if ("RoadID".equals(name)) {
463475 if (!routing && roadIdGenerated > 0)
464476 throw new MapFailedException("found RoadID without Routing=Y in [IMG ID] section in line " + lineNo);
465477 roadHelper.setRoadId(Integer.parseInt(value));
466478 } else if (name.startsWith("Nod")) {
467479 roadHelper.addNode(value);
468 } else if (name.equals("RouteParam") || name.equals("RouteParams")) {
480 } else if ("RouteParam".equals(name) || "RouteParams".equals(name)) {
469481 roadHelper.setParam(value);
470 } else if (name.equals("DirIndicator")) {
482 } else if ("DirIndicator".equals(name)) {
471483 polyline.setDirection(Integer.parseInt(value) > 0);
472484 } else if (name.startsWith("Numbers")) {
473485 roadHelper.addNumbers(parseNumbers(value));
515527 country = strings[nextPos + 2];
516528 nums.setCityInfo(Numbers.LEFT, createCityInfo(city, region, country));
517529 nextPos = 12;
518 } else
530 } else {
519531 nextPos = 10;
532 }
520533 city = strings[nextPos];
521534 if (!"-1".equals(city)){
522535 region = strings[nextPos + 1];
583596 * @see #line
584597 */
585598 private void shape(String name, String value) {
586 if (name.equals("Type")) {
599 if ("Type".equals(name)) {
587600 int type = Integer.decode(value);
588601 if (type == 0x4a00)
589602 type = 0x4a;
594607 } else if (name.startsWith("Data")) {
595608 extractResolution(name);
596609 addLineString(value, true);
610 } else if ("Background".equals(name)) {
611 if ("Y".equals(value))
612 background = true;
597613 }
598614 else {
599615 if(extraAttributes == null)
612628 }
613629
614630 private boolean isCommonValue(MapElement elem, String name, String value) {
615 if (name.equals("Label")) {
631 if ("Label".equals(name)) {
616632 elem.setName(unescape(recode(value)));
617 } else if (name.equals("Label2") || name.equals("Label3")) {
633 } else if ("Label2".equals(name) || "Label3".equals(name)) {
618634 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)) {
620636 try {
621637 endLevel = Integer.valueOf(value);
622638 } catch (NumberFormatException e) {
623639 endLevel = 0;
624640 }
625 } else if (name.equals("ZipCode")) {
641 } else if ("ZipCode".equals(name)) {
626642 elem.setZip(recode(value));
627 } else if (name.equals("CityName")) {
643 } else if ("CityName".equals(name)) {
628644 elem.setCity(recode(value));
629 } else if (name.equals("StreetDesc")) {
645 } else if ("StreetDesc".equals(name)) {
630646 elem.setStreet(recode(value));
631 } else if (name.equals("HouseNumber")) {
647 } else if ("HouseNumber".equals(name)) {
632648 elem.setHouseNumber(recode(value));
633 } else if (name.equals("is_in")) {
649 } else if ("is_in".equals(name)) {
634650 elem.setIsIn(recode(value));
635 } else if (name.equals("Phone")) {
651 } else if ("Phone".equals(name)) {
636652 elem.setPhone(recode(value));
637 } else if (name.equals("CountryName")) {
653 } else if ("CountryName".equals(name)) {
638654 elem.setCountry(unescape(recode(value)));
639 } else if (name.equals("RegionName")) {
655 } else if ("RegionName".equals(name)) {
640656 elem.setRegion(recode(value));
641657 } else {
642658 return false;
764780 */
765781 private int extractResolution(String name) {
766782 currentLevel = Integer.parseInt(name.substring(name.charAt(0) == 'O'? 6: 4));
783 if (currentLevel > 0)
784 dataHighLevel = true;
767785 return extractResolution(currentLevel);
768786 }
769787
794812 * @param value Command value.
795813 */
796814 private void imgId(String name, String value) {
797 if (name.equals("Copyright")) {
815 if ("Copyright".equals(name)) {
798816 copyright = value;
799 } else if (name.equals("Levels")) {
817 } else if ("Levels".equals(name)) {
800818 int nlev = Integer.parseInt(value);
801819 levels = new LevelInfo[nlev];
802820 } else if (name.startsWith("Level")) {
813831 char fc = value.charAt(0);
814832 if (fc == 'm' || fc == 'M')
815833 elevUnits = 'm';
816 } else if (name.equalsIgnoreCase("CodePage")) {
834 } else if ("CodePage".equalsIgnoreCase(name)) {
817835 dec = Charset.forName("cp" + value).newDecoder();
818836 dec.onUnmappableCharacter(CodingErrorAction.REPLACE);
819837 } else if (name.endsWith("LeftSideTraffic")){
853871 * @param value A string representing a lat,long pair.
854872 * @return The coordinate value.
855873 */
856 private Coord makeCoord(String value) {
874 private static Coord makeCoord(String value) {
857875 String[] fields = value.split("[(,)]");
858876
859877 int i = 0;
872890
873891 for(Map.Entry<String, String> entry : extraAttributes.entrySet()) {
874892 String v = entry.getValue();
875 if (entry.getKey().equals("Depth")) {
893 if ("Depth".equals(entry.getKey())) {
876894 String u = extraAttributes.get("DepthUnit");
877895 if("f".equals(u))
878896 v += "ft";
879897 eta.put("depth", v);
880 } else if(entry.getKey().equals("Height")) {
898 } else if("Height".equals(entry.getKey())) {
881899 String u = extraAttributes.get("HeightUnit");
882900 if("f".equals(u))
883901 v += "ft";
884902 eta.put("height", v);
885 } else if(entry.getKey().equals("HeightAboveFoundation")) {
903 } else if("HeightAboveFoundation".equals(entry.getKey())) {
886904 String u = extraAttributes.get("HeightAboveFoundationUnit");
887905 if("f".equals(u))
888906 v += "ft";
889907 eta.put("height-above-foundation", v);
890 } else if(entry.getKey().equals("HeightAboveDatum")) {
908 } else if("HeightAboveDatum".equals(entry.getKey())) {
891909 String u = extraAttributes.get("HeightAboveDatumUnit");
892910 if("f".equals(u))
893911 v += "ft";
894912 eta.put("height-above-datum", v);
895 } else if(entry.getKey().equals("Color")) {
913 } else if("Color".equals(entry.getKey())) {
896914 colour = Integer.decode(v);
897 } else if(entry.getKey().equals("Style")) {
915 } else if("Style".equals(entry.getKey())) {
898916 style = Integer.decode(v);
899 } else if(entry.getKey().equals("Position")) {
917 } else if("Position".equals(entry.getKey())) {
900918 eta.put("position", v);
901 } else if(entry.getKey().equals("FoundationColor")) {
919 } else if("FoundationColor".equals(entry.getKey())) {
902920 eta.put("color", v);
903 } else if(entry.getKey().equals("Light")) {
921 } else if("Light".equals(entry.getKey())) {
904922 eta.put("light", v);
905 } else if(entry.getKey().equals("LightType")) {
923 } else if("LightType".equals(entry.getKey())) {
906924 eta.put("type", v);
907 } else if(entry.getKey().equals("Period")) {
925 } else if("Period".equals(entry.getKey())) {
908926 eta.put("period", v);
909 } else if(entry.getKey().equals("Note")) {
927 } else if("Note".equals(entry.getKey())) {
910928 eta.put("note", v);
911 } else if(entry.getKey().equals("LocalDesignator")) {
929 } else if("LocalDesignator".equals(entry.getKey())) {
912930 eta.put("local-desig", v);
913 } else if(entry.getKey().equals("InternationalDesignator")) {
931 } else if("InternationalDesignator".equals(entry.getKey())) {
914932 eta.put("int-desig", v);
915 } else if(entry.getKey().equals("FacilityPoint")) {
933 } else if("FacilityPoint".equals(entry.getKey())) {
916934 eta.put("facilities", v);
917 } else if(entry.getKey().equals("Racon")) {
935 } else if("Racon".equals(entry.getKey())) {
918936 eta.put("racon", v);
919 } else if(entry.getKey().equals("LeadingAngle")) {
937 } else if("LeadingAngle".equals(entry.getKey())) {
920938 eta.put("leading-angle", v);
921939 }
922940 }
934952 try {
935953 // Proceed only if the restriction is not already marked as invalid.
936954 if (restriction.isValid()) {
937 if (name.equalsIgnoreCase("Nod")) {
955 if ("Nod".equalsIgnoreCase(name)) {
938956 /* ignore */
939 } else if (name.equalsIgnoreCase("TraffPoints")) {
957 } else if ("TraffPoints".equalsIgnoreCase(name)) {
940958 restriction.setTrafficPoints(value);
941 } else if (name.equalsIgnoreCase("TraffRoads")) {
959 } else if ("TraffRoads".equalsIgnoreCase(name)) {
942960 restriction.setTrafficRoads(value);
943 } else if (name.equalsIgnoreCase("RestrParam")) {
961 } else if ("RestrParam".equalsIgnoreCase(name)) {
944962 restriction.setExceptMask(getRestrictionExceptionMask(value));
945 } else if (name.equalsIgnoreCase("Time")) {
963 } else if ("Time".equalsIgnoreCase(name)) {
946964 log.info("Time in restriction definition is ignored " + restriction);
947965 }
948966 }
10291047 }
10301048
10311049 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;
10321052 if (!GType.checkType(kind, type)) {
10331053 throw new MapFailedException("invalid type " + GType.formatType(type) + " for " + kind + ", line " + lineNo);
10341054 }
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 }
8686 }
8787
8888 @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);
9191 assertEquals(russia3.getLongitude(), russia1.getLongitude());
92 Coord russia4 = russia1.destOnRhumLine(10000, 0.0);
92 Coord russia4 = russia1.destOnRhumbLine(10000, 0.0);
9393 assertEquals(russia4.getLongitude(), russia1.getLongitude());
9494 }
9595
473473 List<MapShape> res = smf.merge(Arrays.asList(s1,s2));
474474 assertTrue(testId, res != null);
475475 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 // }
481476 assertEquals(testId, expectedNumPoints, res.get(0).getPoints().size());
482477 // TODO: test shape size
483478 }
205205 }
206206
207207 @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
208222 public void testEqualTagValue() {
209223 Op op = createOp("c!=d & a=$b");
210224 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 }