1if not modules then modules = { } end modules ['m-openstreetmap'] = {
2 version = 1.001,
3 comment = "companion to m-openstreetmap.mkxl",
4 author = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5 copyright = "PRAGMA ADE / ConTeXt Development Team",
6 license = "see context related readme files"
7}
8
9local sin, cos, floor, ceil = math.sin, math.cos, math.floor, math.ceil
10local formatters = string.formatters
11local concat, tohash, sortedhash, setmetatableindex, insert = table.concat, table.tohash, table.sortedhash, table.setmetatableindex, table.insert
12local xmlcollected, xmlparent, xmlfirst, xmlfilter = xml.collected, xml.parent, xml.first, xml.filter
13local P, S, Cs, lpegmatch = lpeg.P, lpeg.S, lpeg, lpeg.match
14
15local openstreetmap = { }
16moduledata.openstreetmap = openstreetmap
17
18local report = logs.reporter("openstreetmap")
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45local polygons = tohash {
46 "abandoned:aeroway", "abandoned:amenity", "abandoned:building",
47 "abandoned:landuse", "abandoned:power", "aeroway", "allotments",
48 "amenity", "area:highway", "craft", "building", "building:part", "club",
49 "golf", "emergency", "harbour", "healthcare", "historic", "landuse",
50 "leisure", "man_made", "military", "natural", "office", "place", "power",
51 "public_transport", "shop", "tourism", "water", "waterway", "wetland",
52
53
54}
55
56
57
58
59
60
61
62
63
64
65local order = {
66 "landuse",
67 "leisure",
68 "natural",
69
70
71 "water",
72 "amenity",
73 "building",
74 "barrier",
75 "man_made",
76 "bridge",
77
78 "historic",
79 "military",
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94 "waterway",
95 "highway",
96 "railway",
97 "aeroway",
98 "aerialway",
99
100
101 "boundary",
102
103}
104
105
106
107
108
109
110
111
112local stacking = {
113 highway = {
114 motorway = 180,
115 trunk = 170,
116 primary = 160,
117 secondary = 150,
118 tertiary = 140,
119 residential = 130,
120 unclassified = 130,
121 road = 130,
122 living_street = 120,
123 pedestrian = 110,
124 raceway = 100,
125 motorway_link = 140,
126 trunk_link = 130,
127 primary_link = 120,
128 secondary_link = 110,
129 tertiary_link = 100,
130 service = 150,
131 track = 110,
132 path = 100,
133 footway = 100,
134 bridleway = 100,
135 cycleway = 100,
136 steps = 190,
137 platform = 190,
138 },
139 railway = {
140 rail = 140,
141 subway = 120,
142 narrow_gauge = 120,
143 light_rail = 120,
144 funicular = 120,
145 preserved = 120,
146 monorail = 120,
147 miniature = 120,
148 turntable = 120,
149 tram = 110,
150 disused = 100,
151 construction = 100,
152 platform = 190,
153 },
154 aeroway = {
155 runway = 160,
156 taxiway = 150,
157 },
158 boundary = {
159 administrative = 0,
160 },
161
162
163
164
165
166
167}
168
169setmetatableindex(stacking,function(t,k)
170 local v = setmetatableindex(function(t,k)
171 t[k] = false return false
172 end)
173 t[k] = v
174 return v
175end)
176
177
178
179local colors = {
180
181
182
183 amenity = {
184 arts_centre = true,
185 bar = true,
186 bicycle_parking = true,
187 college = true,
188 courthouse = true,
189 fountain = true,
190 hospital = true,
191 kindergarten = true,
192 marketplace = true,
193 parking = true,
194 parking_space = true,
195 pharmacy = true,
196 place_of_worship = true,
197 police = true,
198 restaurant = true,
199 school = true,
200 shower = true,
201 social_facility = true,
202 toilets = true,
203 townhall = true,
204
205
206
207
208 bank = true,
209
210 bicycle_parking = true,
211 bicycle_repair_station = true,
212 cafe = true,
213
214 car_wash = true,
215
216 childcare = true,
217 clinic = true,
218
219 clubhouse = true,
220 college = true,
221 community_centre = true,
222
223 computer_lab = true,
224
225 events_venue = true,
226 fast_food = true,
227 fire_station = true,
228 fountain = true,
229 fuel = true,
230
231 library = true,
232 mailroom = true,
233
234
235
236 pharmacy = true,
237 place_of_worship = true,
238
239 post_office = true,
240 recycling = true,
241 research_institute = true,
242
243 theatre = true,
244
245
246
247 wellness_centre = true,
248
249 },
250
251
252
253 boundary = {
254 aboriginal_lands = true,
255 national_park = true,
256 protected_area = true,
257 administrative = true,
258 },
259 building = {
260 apartments = true,
261 bandstand = true,
262 cathedral = true,
263 civic = true,
264 commercial = true,
265 construction = true,
266 garage = true,
267 government = true,
268 hospital = true,
269 house = true,
270 houseboat = true,
271 hut = true,
272 industrial = true,
273 kiosk = true,
274 public = true,
275 residential = true,
276 retail = true,
277 roof = true,
278 school = true,
279 shed = true,
280 townhall = true,
281 yes = true,
282
283 university = true,
284 dormitory = true,
285 barn = true,
286 bridge = true,
287 detached = true,
288 farm_auxiliary = true,
289 grandstand = true,
290 greenhouse = true,
291 kindergarten = true,
292 parking = true,
293 stable = true,
294 stadium = true,
295 toilets = true,
296
297 },
298 emergency = {
299 designated = true,
300 destination = true,
301 no = true,
302 official = true,
303 yes = true,
304 },
305 man_made = {
306 breakwater = true,
307 bridge = true,
308 instrument = true,
309 pier = true,
310 quay = true,
311 tower = true,
312 windmill = true,
313 cutline = true,
314 embankment = true,
315 groyne = true,
316 pipeline = true,
317 },
318 natural = {
319 arete = true,
320 cliff = true,
321 earth_bank = true,
322 ridge = true,
323 sand = true,
324 scrub = true,
325 tree_row = true,
326 water = true,
327 wetland = true,
328 wood = true,
329 fault = true,
330 },
331 barrier = {
332 chain = true,
333 city_wall = true,
334 fence = true,
335 gate = true,
336 guard_rail = true,
337 hedge = true,
338 retaining_wall = true,
339 wall = true,
340 yes = true,
341 },
342 leisure = {
343 garden = true,
344 ice_rink = true,
345 marina = true,
346 park = true,
347 pitch = true,
348 playground = true,
349 slipway = true,
350 sports_centre = true,
351 track = true,
352 beach = true,
353 },
354 boat = {
355 yes = true,
356 },
357 landuse = {
358 allotments = true,
359 cemetery = true,
360 commercial = true,
361 construction = true,
362 forest = true,
363 grass = true,
364 industrial = true,
365 meadow = true,
366 residential = true,
367 static_building = true,
368 village_green = true,
369 },
370 ["bridge:support"] = {
371 pier = true,
372 },
373 golf = {
374 cartpath = true,
375 hole = true,
376 path = true,
377 },
378 area = {
379 yes = true,
380 },
381 bridge = {
382 yes = true,
383 movable = true,
384 viaduct = true,
385 },
386 agricultural = {
387 yes = true,
388 no = true,
389 },
390 historic = {
391 citywalls = true,
392 },
393 tourism = {
394 yes = true,
395 },
396 power = {
397 cable = true,
398 line = true,
399 minor_line = true,
400 },
401 junction = {
402 yes = true,
403 },
404 water = {
405 river = true,
406 basin = true,
407 },
408
409
410
411 highway = {
412 corridor = true,
413 bridleway = true,
414 cycleway = true,
415 footway = true,
416 living_street = true,
417 motorway = true,
418 motorway_link = true,
419 path = true,
420 pedestrian = true,
421 platform = true,
422 primary = true,
423 primary_link = true,
424 raceway = true,
425 residential = true,
426 rest_area = true,
427 road = true,
428
429 secondary_link = true,
430 service = true,
431 services = true,
432 steps = true,
433 tertiary = true,
434 tertiary_link = true,
435 track = true,
436 trunk = true,
437 trunk_link = true,
438 unclassified = true,
439 },
440 waterway = {
441 canal = true,
442 derelict_canal = true,
443 ditch = true,
444 drain = true,
445 river = true,
446 stream = true,
447 tidal_channel = true,
448 wadi = true,
449 weir = true,
450 },
451 railway = {
452 construction = true,
453 disused = true,
454 funicular = true,
455 light_rail = true,
456 miniature = true,
457 monorail = true,
458 narrow_gauge = true,
459 platform = true,
460 preserved = true,
461 rail = true,
462 station = true,
463 subway = true,
464 tram = true,
465 turntable = true,
466 },
467
468 aeroway = {
469 runway = true,
470 taxiway = true,
471 },
472 aerialway = {
473 station = true,
474 },
475}
476
477
478
479
480
481local forcedlines = {
482 golf = { "cartpath", "hole", "path" },
483 emergency = { "designated", "destination", "no", "official", "yes" },
484 historic = { "citywalls" },
485 leisure = { "track", "slipway" },
486 man_made = { "breakwater", "cutline", "embankment", "groyne", "pipeline" },
487 natural = { "cliff", "earth_bank", "tree_row", "ridge", "arete", "fault" },
488 power = { "cable", "line", "minor_line" },
489 tourism = { "yes"},
490 waterway = { "canal", "derelict_canal", "ditch", "drain", "river", "stream", "tidal_channel", "wadi", "weir" },
491}
492
493do
494
495 local function never(t,k) t[k] = false return false end
496
497 for k, v in next, forcedlines do
498 forcedlines[k] = setmetatableindex(tohash(v),never)
499 end
500
501 setmetatableindex(forcedlines,function(t,k)
502 local v = setmetatableindex(never)
503 t[k] = v
504 return v
505 end)
506
507end
508
509
510
511
512for k, v in next, colors do
513 for kk, vv in next, v do
514 v[kk] = "osm:" .. k .. ":" .. kk
515 end
516end
517
518
519
520local lines = {
521 amenity = true,
522 building = true,
523 man_made = true,
524 boat = true,
525}
526
527
528
529
530
531
532local f_f_degree_to_str = formatters["%d°%d’%.0f”"]
533
534local function f_degree_to_str(num)
535 local deg = floor(num)
536 num = (num - deg) * 60
537 local min = floor(num)
538 num = (num - min) * 60
539 local sec = num
540 return f_f_degree_to_str(deg,min,sec)
541end
542
543
544local f_pattern = formatters["/osm/(way|relation)[@visible!='false']/tag[@k='%s']"]
545local f_way = formatters["/osm/way[@id='%s']"]
546local f_relation = formatters["/osm/relation[@id='%s']"]
547
548
549
550
551local f_draw = formatters['D %--t W "%s";']
552local f_fill = formatters['F %--t--C W "%s";']
553local f_both = formatters['P := %--t--C; F P W "%s"; D P W "white" L 2;']
554local f_draw_s = formatters['D %--t W "%s" L %s;']
555local f_fill_s = formatters['F %--t--C W "%s" L %s;']
556local f_both_s = formatters['P := %--t--C; F P W "%s"; D P W "white" L %s;']
557
558local f_nodraw = formatters['ND %--t;']
559local f_nofill = formatters['NF %--t--C;']
560local f_nodraw_s = formatters['ND %--t;']
561local f_nofill_s = formatters['NF %--t--C;']
562
563local f_background = formatters['F %--t -- C W "osm:background";']
564local f_bounds = formatters['setbounds currentpicture to %--t--C withstacking (0,250);']
565local f_clipped = formatters['clip currentpicture to %--t--C withstacking (0,250);']
566
567
568
569
570
571
572
573
574
575
576
577local f_draw_grid_b = formatters['nodraw %--t L 100;']
578local f_draw_grid_e = formatters['dodraw origin dashed %s withcolor "osm:grid" withpen pencircle scaled %N L 100;']
579local f_label_lat = formatters['label.urt((textext("\\infofont\\strut %s")), %s shifted (3,0)) withcolor white L 100;']
580local f_label_lon = formatters['label.top((textext("\\infofont\\strut %s")), %s shifted (0,3)) withcolor white L 100;']
581
582local beginmp = [[
583 begingroup ;
584 pickup pencircle scaled 1 ;
585 save P ; path P ;
586 save D ; let D = draw ;
587 save F ; let F = fill ;
588 save C ; let C = cycle ;
589 save W ; let W = withcolor ;
590 save L ; let L = withstacking ;
591 save ND ; let ND = nodraw ;
592 save DD ; let DD = dodraw ;
593 save NF ; let NF = nofill ;
594 save DF ; let DF = dofill ;
595]]
596
597local endmp = [[
598 endgroup;
599]]
600
601local p_strip = lpeg.Cs( (
602
603
604 (
605 ( P("version") + P("changeset") + P("timestamp") + P("user") + P("uid") )
606 * P('="') * (1-P('"'))^0 * P('"')
607 ) / ""
608 + P(">") * (S("\n\r\t\f ")^1 / "") * P("<")
609 + P('visible="true"') / ""
610 + P(1)
611)^1 )
612
613function openstreetmap.convert(specification)
614
615 local starttime = os.clock()
616 local filename = specification.filename
617
618 if not io.exists(filename) then
619 return
620 end
621
622 report("processing file %a",filename)
623
624
625
626
627
628 local root = io.loaddata(filename)
629 local olds = #root
630 root = lpegmatch(p_strip,root)
631 local news = #root
632
633 report("original size %i bytes, stripped down to %i bytes",olds,news)
634
635 root = xml.convert(root)
636
637 if root.error then
638 report("error in xml",olds,news)
639 return
640 else
641 report("xml data loaded")
642 end
643 local bounds = xmlfirst(root,"/osm/bounds")
644 if not bounds then
645 return
646 end
647
648 local usercolors = specification.used
649 local usedcolors = table.copy(colors)
650
651 if usercolors then
652 for k, v in next, usercolors do
653 local u = usedcolors[k]
654 if not u then
655
656 elseif v == false then
657 usedcolors[k] = false
658
659
660
661 elseif type(v) == "string" then
662 for k in next, u do
663 u[k] = v
664 end
665 elseif type(v) == "table" then
666 for kk, vv in next, v do
667 if vv == false then
668 u[kk] = false
669 elseif type(vv) == "string" then
670 u[kk] = vv
671 end
672 end
673 end
674 end
675 end
676
677
678
679 local minlat = bounds.at.minlat
680 local minlon = bounds.at.minlon
681 local maxlat = bounds.at.maxlat
682 local maxlon = bounds.at.maxlon
683 local midlat = 0.5 * (minlat + maxlat)
684 local deg_to_rad = math.pi / 180.0
685 local scale = 3600
686
687
688
689
690
691 local f_f_pair = formatters["(%.3Ncm,%.3Ncm)"]
692
693 local function f_pair(lon, lat)
694 return f_f_pair((lon - minlon) * scale * cos(midlat * deg_to_rad), (lat-minlat) * scale)
695 end
696
697 local rendering = table.tohash(order)
698 local coordinates = { }
699 local ways = { }
700 local result = { }
701 local r = 0
702 local done = { }
703 local missing = false
704 local layers = { }
705 local areas = { }
706
707 for c in xmlcollected(root,"/osm/node") do
708 local a = c.at
709 coordinates[a.id] = a
710 end
711
712 for c in xmlcollected(root,"/osm/way") do
713 ways[c.at.id] = c
714 end
715
716 for c in xml.collected(root,"tag[@k='area']") do
717 areas[c] = c.at.v
718 end
719
720 for c in xml.collected(root,"tag[@k='layer']") do
721 layers[c] = c.at.v
722 end
723
724
725
726
727 local collected = specification.collect and setmetatableindex(function(t,k)
728 local v = setmetatableindex(function(t,k)
729 local v = {
730 draw = setmetatableindex("table"),
731 fill = setmetatableindex("table"),
732 }
733 t[k] = v
734 return v
735 end)
736 t[k] = v
737 return v
738 end) or false
739
740 local function drawshapes(what,order)
741
742
743
744
745 function xml.expressions.osm(k)
746 return usedcolors[k]
747 end
748
749 local function getcolor(r)
750 local t = xmlfirst(r,"/tag[osm(@k)]")
751 if t then
752 local at = t.at
753 local v = at.v
754 if v ~= "no" then
755 local k = at.k
756 local col = usedcolors[k][v]
757
758 if col then
759
760 return k, col, lines[k], stacking[k][v], forcedlines[k][v]
761 elseif missing then
762 missing[k][v] = (missing[k][v] or 0) + 1
763 end
764 end
765 end
766 end
767
768 local function addpath(r, p, n)
769
770
771
772 for c in xmlcollected(r,"/nd") do
773 local coordinate = coordinates[c.at.ref]
774 if coordinate then
775 n = n + 1 p[n] = f_pair(coordinate.lon, coordinate.lat)
776 end
777 end
778
779
780 return p, n
781 end
782
783 local checkpath = collected and
784
785 function(parent,p,n)
786 local what, color, both, stacking, forced = getcolor(parent)
787 if what and rendering[what] then
788 local where = collected[stacking or order]
789 if not polygons[what] or forced or areas[parent] == "no" then
790 insert(where[color] .draw, f_nodraw(p))
791 elseif both then
792 insert(where[color] .fill, f_nofill(p))
793 insert(where["white"].draw, f_nodraw(p))
794 else
795 insert(where[color] .fill, f_nofill(p))
796 end
797 end
798 end
799
800 or
801
802 function(parent,p,n)
803 local what, color, both, stacking, forced = getcolor(parent)
804 if what and rendering[what] then
805 r = r + 1
806
807
808
809 if not polygons[what] or forced or areas[parent] == "no" then
810 result[r] = stacking and f_draw_s(p,color,stacking) or f_draw(p,color)
811 elseif both then
812 result[r] = stacking and f_both_s(p,color,stacking) or f_both(p,color)
813 else
814 result[r] = stacking and f_fill_s(p,color,stacking) or f_fill(p,color)
815 end
816 end
817 end
818
819
820
821
822
823 for c in xmlcollected(root,f_pattern(what)) do
824 local parent = xmlparent(c)
825 local tag = parent.tg
826 if tag == "way" then
827 local p, n = addpath(parent, { }, 0)
828 if n > 1 then
829 checkpath(parent,p,n)
830 end
831 elseif tag == "relation" then
832 if xmlfilter(parent,"xml://tag[@k='type' and (@v='multipolygon' or @v='boundary' or @v='route')]") then
833 local what, color, both, stacking, forced = getcolor(parent)
834 if rendering[what] then
835 local p, n = { }, 0
836 for m in xmlcollected(parent,"/member[(@type='way') and (@role='outer')]") do
837
838 local f = ways[m.at.ref]
839 if f then
840 p, n = addpath(f,p,n)
841 end
842 end
843 if n > 1 then
844 checkpath(parent,p,n)
845 end
846 end
847 else
848 for m in xmlcollected(parent,"/member[@type='way']") do
849
850 local f = ways[m.at.ref]
851 if f then
852 local p, n = addpath(f, { }, 0)
853 if n > 1 then
854 checkpath(parent,p,n)
855 end
856 end
857 end
858 end
859 end
860 end
861
862 end
863
864
865
866
867 local function drawgrid()
868 local lat0 = ceil (3600*minlat)
869 local lat1 = floor(3600*maxlat)
870 local pen = tonumber(specification.griddot) or 1.5
871 local lat
872 local labels = { }
873 for i=lat0,lat1 do
874 lat = i/3600
875 local p = {
876 f_pair(minlon,lat),
877 f_pair(maxlon,lat),
878 }
879 r = r + 1 result[r] = f_draw_grid_b(p)
880 if i ~= lat0 and i ~= lat1 then
881 labels[#labels+1] = f_label_lat(f_degree_to_str(lat),p[1])
882 end
883 end
884
885 local lon0 = ceil (1800*minlon)*2
886 local lon1 = floor(1800*maxlon)*2
887 local lon
888 for i=lon0,lon1,2 do
889 lon=i/3600
890 local p = {
891 f_pair(lon, minlat),
892 f_pair(lon, maxlat),
893 }
894 r = r + 1 result[r] = f_draw_grid_b(p)
895 if i ~= lon0 and i ~= lon1 then
896 labels[#labels+1] = f_label_lon(f_degree_to_str(lon),p[1])
897 end
898 end
899 r = r + 1 result[r] = f_draw_grid_e("withdots", pen)
900 r = r + 1 result[r] = concat(labels)
901 end
902
903
904
905
906
907
908 local boundary = {
909 f_pair(minlon,minlat),
910 f_pair(maxlon,minlat),
911 f_pair(maxlon,maxlat),
912 f_pair(minlon,maxlat),
913 }
914
915 r = r + 1 result[r] = beginmp
916 r = r + 1 result[r] = f_background(boundary)
917
918
919
920
921
922 for i=1,#order do
923 local o = order[i]
924 if usedcolors[o] then
925 drawshapes(o,i)
926 end
927 end
928
929 if specification.grid == "dots" then
930 drawgrid()
931 end
932
933 if collected then
934
935 local f_flush = formatters[') W "%s" L %s;']
936
937 for stacking, colors in sortedhash(collected) do
938 for color, bunch in next, colors do
939 local draw = bunch.draw
940 local fill = bunch.fill
941 if fill and #fill > 0 then
942 r = r + 1 result[r] = "draw image ("
943 r = r + 1 result[r] = concat(fill)
944 r = r + 1 result[r] = 'DF origin--cycle;'
945 r = r + 1 result[r] = f_flush(color,stacking) ;
946 end
947 if draw and #draw > 0 then
948 r = r + 1 result[r] = "draw image ("
949 r = r + 1 result[r] = concat(draw)
950 r = r + 1 result[r] = 'DD origin;'
951 r = r + 1 result[r] = f_flush(color,stacking+1) ;
952 end
953 end
954 end
955
956 end
957
958
959 r = r + 1 result[r] = f_clipped(boundary)
960 r = r + 1 result[r] = endmp
961
962 if missing then
963 inspect(missing)
964 end
965
966 result = concat(result)
967
968 report("%s characters metapost code, preprocessing time %0.3f seconds",#result,os.clock()-starttime)
969
970 return result
971
972end
973
974function mp.lmt_do_openstreetmap()
975 local specification = metapost.getparameterset("openstreetmap")
976 return openstreetmap.convert(specification)
977end
978 |