m-openstreetmap.lmt /size: 30 Kb    last modification: 2021-10-28 13:51
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-- At one of the bachotex meetings Mojca and I sat down to render the bachotek camp site
21-- as available in openstreetmap. Of course that was a limited setup and after figuring
22-- out some details it went well. Of course we used metapost.
23--
24-- Years later in 2021 on the mailing list there was some discussion on country outlines
25-- and disappointed by the quality of what was referred to I remembered the openstreetmap
26-- adventure. I found that I could export my hometown from the web page so I started from
27-- that: just stepwise seeing what 'ways' passed and assigning them colors. Just as with
28-- the bachotex drawing.
29--
30-- Then I went searching for what tags actually were available and ran into:
31--
32-- https://github.com/openstreetmap/osm2pgsql/blob/master/docs/lua.md
33--
34-- I'm not sure where the script is used, and there are dead links on these pages but some
35-- information can be found in that file so I could combine our findings with these. There
36-- is a whole infrastructure out there with impressive machinery, style sheet generation
37-- etc. but we don't need that here. Also, we don't need to play routes.
38--
39-- Colors are one thing (not too hard with a k/v mapping), but a nest hurdle is to decide
40-- what is a polygon. Actually I found that just checking on something being closed is
41-- rather ok. There are a few extra ones. But we can also use the list in the file mentioned.
42-- We probably need to check it occasionally with the original, although it looks quite
43-- stable.
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 -- "bridge",
54}
55
56-- The amenity tag is sort of overloading the main tag so we put it in the main
57-- hash. This might change.
58--
59-- The stacking order is important and we just started with processing the tags
60-- in a certain order. However, in \LUAMETATEX\ we can now use stacking so that
61-- makes for a nice test. At the cost of a bit more metapost code and conversion
62-- time we can use that. Anyway, this is the list we use and it's an adaption
63-- from the bachitex one.
64
65local order = {
66    "landuse",
67    "leisure",
68    "natural",
69 -- "geological",
70
71    "water",
72    "amenity",
73    "building",
74    "barrier",
75    "man_made",
76    "bridge",
77
78    "historic",
79    "military",
80
81 -- "office",
82 -- "craft",
83
84 -- "emergency",
85 -- "healthcare",
86 -- "place",
87 -- "power",
88 -- "shop",
89 -- "sport",
90 -- "telecom",
91
92 -- "publictransport",
93
94    "waterway",
95    "highway",
96    "railway",
97    "aeroway",
98    "aerialway",
99
100 -- "tourism",
101    "boundary", -- overlaps ! not okay for hasselt.osm
102 -- "route",
103}
104
105-- From the mentioned lua file we get the stacking (funny numbers are used):
106
107-- In Hasselt we have a combination, highway should win :
108--
109--  <tag k="bridge" v="yes"/>
110--  <tag k="highway" v="footway"/>
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 -- bridge = {
163 --     yes            = 103,
164 --     movable        = 102,
165 --     viaduct        = 103,
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-- reservoir_covered: @ bachotex
178
179local colors = {
180
181    -- these concern details:
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     -- university                = true, -- no, it will mark all red .. maybe some other color so we need stacking
206
207     -- atm                       = true,
208        bank                      = true,
209     -- bbq                       = true,
210        bicycle_parking           = true,
211        bicycle_repair_station    = true,
212        cafe                      = true,
213     -- car_sharing               = true,
214        car_wash                  = true,
215     -- charging_station          = true,
216        childcare                 = true,
217        clinic                    = true,
218     -- clock                     = true,
219        clubhouse                 = true,
220        college                   = true,
221        community_centre          = true,
222     -- compressed_air            = true,
223        computer_lab              = true,
224     -- drinking_water            = true,
225        events_venue              = true,
226        fast_food                 = true,
227        fire_station              = true,
228        fountain                  = true,
229        fuel                      = true,
230     -- ice_cream                 = true,
231        library                   = true,
232        mailroom                  = true,
233     -- microwave                 = true,
234     -- parking_entrance          = true,
235     -- parking_space             = true,
236        pharmacy                  = true,
237        place_of_worship          = true,
238     -- post_box                  = true,
239        post_office               = true,
240        recycling                 = true,
241        research_institute        = true,
242     -- social_facility           = true,
243        theatre                   = true,
244     -- vending_machine           = true,
245     -- waste_basket              = true,
246     -- waste_disposal            = true,
247        wellness_centre           = true,
248
249    },
250
251    -- these are basic:
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, -- no roadtrip
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 = { -- funny, this category
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    -- these indicate routes:
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     -- secondary        = true,
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-- We use this 'inside' knowledge encoded in the mentioned script to avoid bad fills
478-- (for instance polygons can be unconnected and unordered). We define the table a bit
479-- different.
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-- For fast checking we apply the usual context tricks:
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-- We draw and fill but sometimes we do both:
519
520local lines = {
521    amenity  = true,
522    building = true,
523    man_made = true,
524    boat     = true,
525}
526
527-- When checking labels for a while I had a blacklist of tags (keys) but
528-- dropped that when most was done. So, we're now ready for the real deal:
529
530-- this only works for positive numbers
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-- local f_pattern     = formatters["/osm/(way|relation)[@visible='true']/tag[@k='%s']"]
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-- We could reuse paths and concat 0,n in the call but when testing it the gain was
549-- not much so I went for readability.
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-- For now no labels are printed, also because that's now what we use this for. At
568-- some point I will provide some hooks to put text at coordinates.
569
570-- local f_draw_p      = formatters["path p ; p := %--t ; D p W %s;"]
571-- local f_fill_p      = formatters["path p ; p := %--t -- C ; F p W %s;"]
572-- local f_textext     = formatters['draw (textext("\\bf %s") scaled 0.35) shifted center p withcolor white;']
573
574-- Grids were also part of the original code, so I kept that. It's done a bit more
575-- efficient by using a single path.
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--  (P([[<tag k="TMC:]]) * (1 - P("/>"))^1 * P("/>") * S("\n\r\t\f ")^0) / ""
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    -- we can consider stripping crap first
625
626 -- local root   = xml.load(filename)
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                -- error
656            elseif v == false then
657                usedcolors[k] = false
658             -- for k in next, u do
659             --     u[k] = false
660             -- end
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 -- inspect(usedcolors)
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 -- vertical scale: 1" = 1cm
686
687 -- local function f_pair(lon, lat)
688 --     return formatters("(%.3Ncm,%.3Ncm)", (lon - minlon) * scale * cos(midlat * deg_to_rad), (lat-minlat) * scale)
689 -- end
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 -- setmetatableindex("table")
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    -- Collecting is more a private option. It doesn't save much on the output
725    -- but looks better with transparency, which makes no sense using anyway.
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        -- see bachotex rendering for an example
743        -- also layer and polygon
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                    -- we need an example: if layers[r] then print(layers[r]) end
758                    if col then
759                        -- todo : trace colors and stacking
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         -- if done[r] then
770         --     print("AGAIN")
771         -- else
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         --     done[r] = true
779         -- end
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                 -- if not stacking then
807                 --     stacking = order
808                 -- end
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        -- There are ways and relations. Relations can have members that point to
820        -- ways but also relations. My impression is that we can stick to way members
821        -- but I'll deal with that when needed.
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                         -- local f = xmlfirst(root,f_way(m.at.ref))
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                     -- local f = xmlfirst(root,f_way(m.at.ref))
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    -- As with the other latitude and longitude mapping calculations the next magick
865    -- comes from Mojca.
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    -- We add a background first and clip later. Beware: There can be substantial bits
904    -- outside the clip path (like rivers) but because paths are not that detailed we
905    -- don't waste time on building a cycle. We could check if points are outside the
906    -- boundingbox and then use the metapost buildpath macro .. some day.
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 -- r = r + 1 result[r] = "drawoptions (withtransparency (1,.5)) ;"
919
920    -- use stacking instead
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--     r = r + 1 result[r] = f_bounds(boundary)
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