├── vector_tiles.mbtiles
├── static
├── leaflet
│ ├── images
│ │ ├── layers.png
│ │ ├── layers-2x.png
│ │ ├── route_end.png
│ │ ├── marker-icon.png
│ │ ├── marker-shadow.png
│ │ ├── route_end_2x.png
│ │ └── marker-icon-2x.png
│ ├── leaflet.polyline.js
│ └── leaflet.css
├── index.html
├── osmgraph.js
├── conflation-browser.js
└── mbgl
│ └── mapbox-gl.css
├── config.ru
├── config.json
├── process.lua
├── server.rb
└── README.md
/vector_tiles.mbtiles:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/systemed/conflation/HEAD/vector_tiles.mbtiles
--------------------------------------------------------------------------------
/static/leaflet/images/layers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/systemed/conflation/HEAD/static/leaflet/images/layers.png
--------------------------------------------------------------------------------
/static/leaflet/images/layers-2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/systemed/conflation/HEAD/static/leaflet/images/layers-2x.png
--------------------------------------------------------------------------------
/static/leaflet/images/route_end.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/systemed/conflation/HEAD/static/leaflet/images/route_end.png
--------------------------------------------------------------------------------
/static/leaflet/images/marker-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/systemed/conflation/HEAD/static/leaflet/images/marker-icon.png
--------------------------------------------------------------------------------
/static/leaflet/images/marker-shadow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/systemed/conflation/HEAD/static/leaflet/images/marker-shadow.png
--------------------------------------------------------------------------------
/static/leaflet/images/route_end_2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/systemed/conflation/HEAD/static/leaflet/images/route_end_2x.png
--------------------------------------------------------------------------------
/static/leaflet/images/marker-icon-2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/systemed/conflation/HEAD/static/leaflet/images/marker-icon-2x.png
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | # Run locally with rackup -p 8080
2 |
3 | puts "Loading conflation server"
4 | require './server'
5 |
6 | use Rack::Static,
7 | urls: ["/conflation-browser.js", "/index.html", "/osmgraph.js", "/spec.json", "/mbgl", "/leaflet"],
8 | root: "static",
9 | header_rules: [[/.json\z/, { "Content-Type"=>"application/json" }],
10 | [/.png\z/, { "Content-Type"=>"image/png" }],
11 | [/.pbf\z/, { "Content-Type"=>"application/octet-stream" }]]
12 | use Rack::Reloader, 0
13 | use Rack::ShowExceptions
14 |
15 | run ConflationServer.new("vector_tiles.mbtiles")
16 |
--------------------------------------------------------------------------------
/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "layers": {
3 | "crossings": { "minzoom": 9, "maxzoom": 14, "source": "shp/OOCIE_Crossing.shp", "source_columns": true },
4 | "hazards": { "minzoom": 9, "maxzoom": 14, "source": "shp/OOCIE_Hazard.shp", "source_columns": true },
5 | "junctions": { "minzoom": 9, "maxzoom": 14, "source": "shp/OOCIE_Junction.shp", "source_columns": true },
6 | "routes": { "minzoom": 9, "maxzoom": 14, "source": "shp/OOCIE_Route.shp", "source_columns": true },
7 | "signals": { "minzoom": 9, "maxzoom": 14, "source": "shp/OOCIE_Traffic_Signal.shp", "source_columns": true }
8 | },
9 | "settings": {
10 | "minzoom": 7,
11 | "maxzoom": 14,
12 | "basezoom": 14,
13 | "include_ids": false,
14 | "name": "OOCIE",
15 | "version": "0.1",
16 | "description": "Test vector tiles for OOCIE",
17 | "compress": "gzip",
18 | "bounding_box": [-1.5553,51.7500,-1.3293,51.8833]
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/process.lua:
--------------------------------------------------------------------------------
1 | hazards = { ["Barrier Types"] = { barrier = "yes"} ,
2 | ["Lamp Posts"] = { highway = "street_lamp" },
3 | ["Traffic Calming"] = { traffic_calming = "yes" },
4 | ["Bus Stops"] = { highway = "bus_stop" } }
5 |
6 | routes = { ["Off-Road"] = { highway = "cycleway" } ,
7 | ["On-Road"] = { _match_key = "highway" } ,
8 | ["Parallel"] = { highway = "cycleway" } }
9 |
10 | lights = { ["None"] = { lit = "no" },
11 | ["Street"] = { lit = "yes" } }
12 |
13 | surfaces= { ["Coloured Surface"] = { surface = "asphalt" },
14 | ["Tarmac"] = { surface = "asphalt" },
15 | ["Unbound Hardcore"] = { surface = "gravel" } }
16 |
17 | widths = { ["Narrow"] = { width = "<1.5m" },
18 | ["Very Narrow"] = { width = "<1m" } }
19 |
20 | speeds = { ["30"] = { maxspeed = "30 mph" },
21 | ["40"] = { maxspeed = "40 mph" },
22 | ["50"] = { maxspeed = "50 mph" },
23 | ["NSL"]= { maxspeed = "60 mph" } }
24 |
25 | crossings={ ["Refuge"]= { highway = "crossing", crossing = "island", _filter = "waynode:highway", _match_key = "" },
26 | ["Zebra"] = { highway = "crossing", crossing = "zebra", _filter = "waynode:highway", _match_key = "" } }
27 |
28 | function attribute_function(attr)
29 | local tags = {}
30 |
31 | remap_tags(tags,attr,"HazardType", hazards)
32 | remap_tags(tags,attr,"RouteType" , routes)
33 | remap_tags(tags,attr,"Lighting" , lights)
34 | remap_tags(tags,attr,"SurfaceMat", surfaces)
35 | remap_tags(tags,attr,"CycleLaneW", widths)
36 | remap_tags(tags,attr,"CycleTrack", widths)
37 | remap_tags(tags,attr,"SpeedLimit", speeds)
38 | remap_tags(tags,attr,"CrossingTy", crossings)
39 |
40 | if attr["Comment"] and attr["Comment"]~="" then tags["_comment"]=attr["Comment"] end
41 | tags["id"] = attr["GlobalID"]
42 | return tags
43 | end
44 |
45 | function remap_tags(tags,attr,key,hash)
46 | if attr[key] then
47 | local kv = hash[attr[key]]
48 | if kv then
49 | for k,v in pairs(kv) do
50 | tags[k] = v
51 | end
52 | end
53 | end
54 | end
55 |
56 | function node_function(node)
57 | end
58 |
59 | function way_function(way)
60 | end
61 |
62 | node_keys = {}
63 |
--------------------------------------------------------------------------------
/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | conflation-browser
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
18 |
60 |
61 |
62 |
63 |
64 |
65 |
Candidate - of -
66 |
67 |
68 |
69 |
70 |
71 |
72 |
80 |
81 |
--------------------------------------------------------------------------------
/server.rb:
--------------------------------------------------------------------------------
1 | require 'sqlite3'
2 | begin; require 'glug' # Optional glug dependency
3 | rescue LoadError; end # |
4 |
5 | class ConflationServer
6 |
7 | CONTENT_TYPES = {
8 | json: "application/json",
9 | png: "image/png",
10 | pbf: "application/octet-stream",
11 | css: "text/css",
12 | js: "application/javascript",
13 | }
14 |
15 | def initialize(mbtiles)
16 | @@mbtiles = mbtiles
17 | ConflationServer.connect
18 | end
19 |
20 | def self.connect
21 | @@db = SQLite3::Database.new(@@mbtiles)
22 | Dir.chdir("static") unless Dir.pwd.include?("static")
23 | self
24 | end
25 |
26 | def call(env)
27 | path = (env['REQUEST_PATH'] || env['REQUEST_URI']).sub(/^\//,'')
28 | if path.empty? then path='index.html' end
29 | if path =~ %r!(\d+)/(\d+)/(\d+).*\.pbf!
30 | # Serve .pbf tile from mbtiles
31 | z,x,y = $1.to_i, $2.to_i, $3.to_i
32 | tms_y = 2**z - y - 1
33 | res = @@db.execute("SELECT tile_data FROM tiles WHERE zoom_level=? AND tile_column=? AND tile_row=?", [z, x, tms_y])
34 | if res.length>0
35 | blob = res[0][0]
36 | ['200', {
37 | 'Content-Type' => 'application/x-protobuf',
38 | 'Content-Encoding'=> 'gzip',
39 | 'Content-Length' => blob.bytesize.to_s,
40 | 'Cache-Control' => 'max-age=0',
41 | 'Access-Control-Allow-Origin' => '*'
42 | }, [blob]]
43 | else
44 | puts "Empty #{z}/#{x}/#{y}"
45 | ['204', {}, ["Resource at #{path} not found"]]
46 | end
47 |
48 | elsif path=='metadata'
49 | # Serve mbtiles metadata
50 | doc = {}
51 | @@db.execute("SELECT name,value FROM metadata").each do |row|
52 | k,v = row
53 | doc[k] = k=='json' ? JSON.parse(v) : v
54 | end
55 | ['200', {'Content-Type' => 'application/json', 'Cache-Control' => 'max-age=0' }, [doc.to_json]]
56 |
57 | elsif File.exist?(path)
58 | # Serve static file
59 | ct = path.match(/\.(\w+)$/) ? (CONTENT_TYPES[$1.to_sym] || 'text/html') : 'text/html'
60 | ['200', {'Content-Type' => ct, 'Cache-Control' => 'max-age=0'}, [File.read(path)]]
61 |
62 | else
63 | # Not found
64 | puts "Couldn't find #{path}"
65 | ['404', {'Content-Type' => 'text/html'}, ["Resource at #{path} not found"]]
66 | end
67 | end
68 |
69 | # Start server
70 |
71 | if defined?(PhusionPassenger)
72 | puts "Starting Passenger server"
73 | PhusionPassenger.on_event(:starting_worker_process) do |forked|
74 | if forked then ConflationServer.connect end
75 | end
76 |
77 | else
78 | puts "Starting local server"
79 | require 'rack'
80 |
81 | server = ConflationServer.new(ARGV[0])
82 | app = Proc.new do |env|
83 | server.call(env)
84 | end
85 | Rack::Handler::WEBrick.run(app)
86 | end
87 | end
88 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ### OpenStreetMap Live Conflation
2 |
3 | OSM Live Conflation provides a visual interface for manually merging third-party datasets into OpenStreetMap.
4 |
5 | Your third-party data is encoded into vector tiles (Mapbox MVT format). The UI enables mappers to select items from the vector tiles and either transfer their attributes to existing OSM objects, or create new objects. By calling the OSM API to find nearby features and then matching based on proximity and tag similarity, the most likely candidates are presented first. Once the mapper has merged their chosen features, the resulting changes can be uploaded direct to the OSM API.
6 |
7 | ### Requirements
8 |
9 | The server is written in Ruby. The sqlite gem is required (to serve vector tiles in .mbtiles format), and either rack (for a local webserver) or Phusion Passenger (for deployment). If deploying with Phusion Passenger, use the supplied config.ru and point your Apache root to /srv/yoursitename/static .
10 |
11 | Vector tiles are prepared using [tilemaker](https://github.com/systemed/tilemaker). You are recommended to build the latest version from source.
12 |
13 | ### Getting up and running
14 |
15 | Make sure your input data is in shapefile format, EPSG 4326 projection (simple WGS84 lat/long). You can convert most vector formats to shapefile using ogr2ogr, for example:
16 |
17 | ogr2ogr -f "ESRI Shapefile" -t_srs "EPSG:4326" shp OOCIE_Extract_20190214.gdb.zip
18 |
19 | Then create vector tiles from the data using tilemaker. To do this you'll need to write two config files, examples of which are provided.
20 |
21 | * config.json lists the shapefiles that you want to read; what zoom levels each should show up at; and the bounding box for your project.
22 | * process.lua is a Lua script that reads the shapefile attributes and converts them to OSM tags. This is where you put your tag remapping logic. `attribute_function` is called with a table (hash) of shapefile attributes and must return a table (hash) of OSM tags.
23 |
24 | Make sure you're in the directory containing these two files, then simply
25 |
26 | tilemaker --output vector_tiles.mbtiles
27 |
28 | The result is an .mbtiles file containing vector tiles with all your data. (An example is provided: delete this before creating your own.)
29 |
30 | You can now spin up the server. To run it locally:
31 |
32 | ruby server.rb vector_tiles.mbtiles
33 |
34 | Then open the site at http://localhost:8080/index.html .
35 |
36 | ### Using OSM Live Conflation
37 |
38 | 
39 |
40 | Your source data is on the left, OSM on the right.
41 |
42 | The points and lines from your source data are overlaid on a satellite map. Clicking on any of these will identify OSM candidates to be modified, or a new geometry to be created. Use the 'Next >' button to page through the candidates. Each candidate is highlighted on the OSM map (top right) as you do so.
43 |
44 | Once you've chosen one, you can use the checkboxes to deselect any tags you don't want to be applied. Click 'Accept' to make the change. The source feature is temporarily removed from the left-hand map when you do so. (If you don't want to remove it - for example, if a source feature maps to more than one OSM feature - then click 'Accept and keep open').
45 |
46 | To upload your changes, enter your OSM username and password into the input fields; click 'Upload'; and enter a changeset comment.
47 |
48 | Keyboard shortcuts are available: 1-9 to toggle tags, Space to cycle through candidates, Enter to accept, Delete to ignore.
49 |
50 | ### Using vector tiles in iD
51 |
52 | You can also load your vector tiles directly into iD, OpenStreetMap's default online editor.
53 |
54 | In iD, click the 'Map data' icon on the right, then '...' by 'Custom Map Data'. In the dialogue that appears, enter a URL like https://url.of.your.server/{z}/{x}/{y}.pbf .
55 |
56 | ### Advanced tag remapping
57 |
58 | When rewriting tags into vector tiles, you can add special keys/values. Currently the following are supported:
59 |
60 | * You should always add an "id" key with a value unique to that feature. (Since a feature may cross vector tile boundaries, this enables features to be consistently removed from the source map display.)
61 | * The key "_match_key" indicates that candidates must have a tag with that key (e.g. _match_key=highway)
62 | * A key "_filter", with value "waynode:highway", indicates that candidates must be nodes within a highway way
63 | * Any other key beginning with "_" will be ignored (useful for comments)
64 |
65 | ### About this project
66 |
67 | Work on this project was supported by the Open Data Institute via Oxfordshire County Council: https://theodi.org/article/the-projects-were-funding-to-explore-open-geospatial-data-in-local-government/
68 |
69 | See https://github.com/systemed/conflation/issues/1 for a to-do list of identified enhancements.
70 |
71 | Map rendering is via [Mapbox GL](https://github.com/mapbox/mapbox-gl-js) and [Leaflet](https://leafletjs.com).
72 |
73 | MIT licence, (c) Richard Fairhurst 2019.
74 |
--------------------------------------------------------------------------------
/static/leaflet/leaflet.polyline.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Utility functions to decode/encode numbers and array's of numbers
3 | * to/from strings (Google maps polyline encoding)
4 | *
5 | * Extends the L.Polyline and L.Polygon object with methods to convert
6 | * to and create from these strings.
7 | *
8 | * Jan Pieter Waagmeester
9 | *
10 | * Original code from:
11 | * http://facstaff.unca.edu/mcmcclur/GoogleMaps/EncodePolyline/
12 | * (which is down as of december 2014)
13 | */
14 |
15 | (function () {
16 | 'use strict';
17 |
18 | var defaultOptions = function (options) {
19 | if (typeof options === 'number') {
20 | // Legacy
21 | options = {
22 | precision: options
23 | };
24 | } else {
25 | options = options || {};
26 | }
27 |
28 | options.precision = options.precision || 5;
29 | options.factor = options.factor || Math.pow(10, options.precision);
30 | options.dimension = options.dimension || 2;
31 | return options;
32 | };
33 |
34 | var PolylineUtil = {
35 | encode: function (points, options) {
36 | options = defaultOptions(options);
37 |
38 | var flatPoints = [];
39 | for (var i = 0, len = points.length; i < len; ++i) {
40 | var point = points[i];
41 |
42 | if (options.dimension === 2) {
43 | flatPoints.push(point.lat || point[0]);
44 | flatPoints.push(point.lng || point[1]);
45 | } else {
46 | for (var dim = 0; dim < options.dimension; ++dim) {
47 | flatPoints.push(point[dim]);
48 | }
49 | }
50 | }
51 |
52 | return this.encodeDeltas(flatPoints, options);
53 | },
54 |
55 | decode: function (encoded, options) {
56 | options = defaultOptions(options);
57 |
58 | var flatPoints = this.decodeDeltas(encoded, options);
59 |
60 | var points = [];
61 | for (var i = 0, len = flatPoints.length; i + (options.dimension - 1) < len;) {
62 | var point = [];
63 |
64 | for (var dim = 0; dim < options.dimension; ++dim) {
65 | point.push(flatPoints[i++]);
66 | }
67 |
68 | points.push(point);
69 | }
70 |
71 | return points;
72 | },
73 |
74 | encodeDeltas: function (numbers, options) {
75 | options = defaultOptions(options);
76 |
77 | var lastNumbers = [];
78 |
79 | for (var i = 0, len = numbers.length; i < len;) {
80 | for (var d = 0; d < options.dimension; ++d, ++i) {
81 | var num = numbers[i];
82 | var delta = num - (lastNumbers[d] || 0);
83 | lastNumbers[d] = num;
84 |
85 | numbers[i] = delta;
86 | }
87 | }
88 |
89 | return this.encodeFloats(numbers, options);
90 | },
91 |
92 | decodeDeltas: function (encoded, options) {
93 | options = defaultOptions(options);
94 |
95 | var lastNumbers = [];
96 |
97 | var numbers = this.decodeFloats(encoded, options);
98 | for (var i = 0, len = numbers.length; i < len;) {
99 | for (var d = 0; d < options.dimension; ++d, ++i) {
100 | numbers[i] = Math.round((lastNumbers[d] = numbers[i] + (lastNumbers[d] || 0)) * options.factor) / options.factor;
101 | }
102 | }
103 |
104 | return numbers;
105 | },
106 |
107 | encodeFloats: function (numbers, options) {
108 | options = defaultOptions(options);
109 |
110 | for (var i = 0, len = numbers.length; i < len; ++i) {
111 | numbers[i] = Math.round(numbers[i] * options.factor);
112 | }
113 |
114 | return this.encodeSignedIntegers(numbers);
115 | },
116 |
117 | decodeFloats: function (encoded, options) {
118 | options = defaultOptions(options);
119 |
120 | var numbers = this.decodeSignedIntegers(encoded);
121 | for (var i = 0, len = numbers.length; i < len; ++i) {
122 | numbers[i] /= options.factor;
123 | }
124 |
125 | return numbers;
126 | },
127 |
128 | encodeSignedIntegers: function (numbers) {
129 | for (var i = 0, len = numbers.length; i < len; ++i) {
130 | var num = numbers[i];
131 | numbers[i] = (num < 0) ? ~(num << 1) : (num << 1);
132 | }
133 |
134 | return this.encodeUnsignedIntegers(numbers);
135 | },
136 |
137 | decodeSignedIntegers: function (encoded) {
138 | var numbers = this.decodeUnsignedIntegers(encoded);
139 |
140 | for (var i = 0, len = numbers.length; i < len; ++i) {
141 | var num = numbers[i];
142 | numbers[i] = (num & 1) ? ~(num >> 1) : (num >> 1);
143 | }
144 |
145 | return numbers;
146 | },
147 |
148 | encodeUnsignedIntegers: function (numbers) {
149 | var encoded = '';
150 | for (var i = 0, len = numbers.length; i < len; ++i) {
151 | encoded += this.encodeUnsignedInteger(numbers[i]);
152 | }
153 | return encoded;
154 | },
155 |
156 | decodeUnsignedIntegers: function (encoded) {
157 | var numbers = [];
158 |
159 | var current = 0;
160 | var shift = 0;
161 |
162 | for (var i = 0, len = encoded.length; i < len; ++i) {
163 | var b = encoded.charCodeAt(i) - 63;
164 |
165 | current |= (b & 0x1f) << shift;
166 |
167 | if (b < 0x20) {
168 | numbers.push(current);
169 | current = 0;
170 | shift = 0;
171 | } else {
172 | shift += 5;
173 | }
174 | }
175 |
176 | return numbers;
177 | },
178 |
179 | encodeSignedInteger: function (num) {
180 | num = (num < 0) ? ~(num << 1) : (num << 1);
181 | return this.encodeUnsignedInteger(num);
182 | },
183 |
184 | // This function is very similar to Google's, but I added
185 | // some stuff to deal with the double slash issue.
186 | encodeUnsignedInteger: function (num) {
187 | var value, encoded = '';
188 | while (num >= 0x20) {
189 | value = (0x20 | (num & 0x1f)) + 63;
190 | encoded += (String.fromCharCode(value));
191 | num >>= 5;
192 | }
193 | value = num + 63;
194 | encoded += (String.fromCharCode(value));
195 |
196 | return encoded;
197 | }
198 | };
199 |
200 | // Export Node module
201 | if (typeof module === 'object' && typeof module.exports === 'object') {
202 | module.exports = PolylineUtil;
203 | }
204 |
205 | // Inject functionality into Leaflet
206 | if (typeof L === 'object') {
207 | if (!(L.Polyline.prototype.fromEncoded)) {
208 | L.Polyline.fromEncoded = function (encoded, options) {
209 | return L.polyline(PolylineUtil.decode(encoded), options);
210 | };
211 | }
212 | if (!(L.Polygon.prototype.fromEncoded)) {
213 | L.Polygon.fromEncoded = function (encoded, options) {
214 | return L.polygon(PolylineUtil.decode(encoded), options);
215 | };
216 | }
217 |
218 | var encodeMixin = {
219 | encodePath: function () {
220 | return PolylineUtil.encode(this.getLatLngs());
221 | }
222 | };
223 |
224 | if (!L.Polyline.prototype.encodePath) {
225 | L.Polyline.include(encodeMixin);
226 | }
227 | if (!L.Polygon.prototype.encodePath) {
228 | L.Polygon.include(encodeMixin);
229 | }
230 |
231 | L.PolylineUtil = PolylineUtil;
232 | }
233 | })();
--------------------------------------------------------------------------------
/static/osmgraph.js:
--------------------------------------------------------------------------------
1 | class OSMGraph {
2 | constructor(serverURL) {
3 | this.nodes = {};
4 | this.ways = {};
5 | this.relations = {};
6 | this.negativeID = 0;
7 | this.server = serverURL;
8 | }
9 |
10 | // Set poi flag on all nodes
11 | setPOIFlag() {
12 | for (var id in this.ways) {
13 | var w = this.ways[id];
14 | for (var nd of w.nodes) {
15 | nd.poi = false;
16 | }
17 | }
18 | }
19 |
20 | // Utility methods
21 | static fastDistance(lat1,lon1,lat2,lon2) {
22 | var deg2rad = Math.PI / 180;
23 | lat1 *= deg2rad;
24 | lon1 *= deg2rad;
25 | lat2 *= deg2rad;
26 | lon2 *= deg2rad;
27 | var diam = 12742000;
28 | var dLat = lat2 - lat1;
29 | var dLon = lon2 - lon1;
30 | var a = (
31 | (1 - Math.cos(dLat)) +
32 | (1 - Math.cos(dLon)) * Math.cos(lat1) * Math.cos(lat2)
33 | ) / 2;
34 | return diam * Math.asin(Math.sqrt(a));
35 | }
36 |
37 | // XML parser
38 | parseFromXML(doc) {
39 | // Parse nodes
40 | for (var n of doc.getElementsByTagName('node')) {
41 | var node = new Node(
42 | n.attributes['id'].value,
43 | OSMGraph.parseTags(n),
44 | n.attributes['version'].value,
45 | n.attributes['lat'].value,
46 | n.attributes['lon'].value
47 | );
48 | if (this.nodes[node.id] && this.nodes[node.id].dirty) continue;
49 | this.nodes[node.id] = node;
50 | }
51 | // Parse ways
52 | for (var w of doc.getElementsByTagName('way')) {
53 | var nodelist = [];
54 | for (var nd of w.getElementsByTagName('nd')) {
55 | var nid = nd.attributes['ref'].value;
56 | if (this.nodes[nid]) nodelist.push(this.nodes[nid]);
57 | }
58 | var way = new Way(
59 | w.attributes['id'].value,
60 | OSMGraph.parseTags(w),
61 | w.attributes['version'].value,
62 | nodelist
63 | );
64 | if (this.ways[way.id] && this.ways[way.id].dirty) continue;
65 | this.ways[way.id] = way;
66 | }
67 | // Parse relations
68 | for (var r of doc.getElementsByTagName('relation')) {
69 | var memberlist = [];
70 | for (var m of r.getElementsByTagName('member')) {
71 | var t = m.attributes['type'].value;
72 | var id = m.attributes['ref' ].value;
73 | var rl = m.attributes['role'].value;
74 | if (t=='relation') continue; // noooope
75 | var obj= t=='way' ? this.ways[id] : this.nodes[id];
76 | if (obj) memberlist.push(new RelationMember(obj, rl));
77 | }
78 | var relation = new Relation(
79 | r.attributes['id'].value,
80 | OSMGraph.parseTags(r),
81 | r.attributes['version'].value,
82 | memberlist
83 | );
84 | if (this.relations[relation.id] && this.relations[relation.id].dirty) continue;
85 | this.relations[relation.id] = relation;
86 | }
87 | }
88 | static parseTags(obj) {
89 | var tags = {};
90 | for (var t of obj.getElementsByTagName('tag')) {
91 | tags[t.attributes['k'].value] = t.attributes['v'].value;
92 | }
93 | return tags;
94 | }
95 | static tagsToXML(osmObj,xml,xmlObj) {
96 | for (var k in osmObj.tags) {
97 | var tag = xml.createElement("tag");
98 | tag.setAttribute("k",k);
99 | tag.setAttribute("v",osmObj.tags[k]);
100 | xmlObj.appendChild(tag);
101 | }
102 | }
103 |
104 | // Find dirty objects
105 | dirtyObjects() {
106 | var id, dirty = { ways: [], nodes: [], relations: [] };
107 | for (id in this.nodes ) { if (this.nodes[id].dirty ) { dirty.nodes.push(this.nodes[id]); } }
108 | for (id in this.ways ) { if (this.ways[id].dirty ) { dirty.ways.push(this.ways[id]); } }
109 | for (id in this.relations) { if (this.relations[id].dirty) { dirty.relations.push(this.relations[id]); } }
110 | return dirty;
111 | }
112 |
113 | // Add an object
114 | add(obj) {
115 | if (obj.constructor.name=='Way' ) { this.ways[obj.id] = obj; }
116 | else if (obj.constructor.name=='Node' ) { this.nodes[obj.id] = obj; }
117 | else if (obj.constructor.name=='Relation') { this.relations[obj.id] = obj; }
118 | }
119 |
120 | // Get a negative ID
121 | nextNegative() {
122 | this.negativeID--;
123 | return this.negativeID;
124 | }
125 |
126 | // Open a changeset
127 | openChangeset(username, password, changesetTags, successFunction) {
128 | // Create changeset XML
129 | var xml = document.implementation.createDocument(null,null);
130 | var osm = xml.createElement("osm");
131 | var changeset = xml.createElement("changeset");
132 | for (var k in changesetTags) {
133 | var tag = xml.createElement("tag");
134 | tag.setAttribute('k',k);
135 | tag.setAttribute('v',changesetTags[k]);
136 | changeset.appendChild(tag);
137 | }
138 | osm.appendChild(changeset);
139 | xml.appendChild(osm);
140 | // Send to OSM
141 | fetch(this.server+"/api/0.6/changeset/create", {
142 | method: "PUT",
143 | headers: { "Content-Type": "text/xml",
144 | "Authorization": "Basic " + window.btoa(username + ":" + password) },
145 | body: new XMLSerializer().serializeToString(xml)
146 | }).then(response => {
147 | response.text().then(text => {
148 | if (isNaN(text)) {
149 | successFunction(false);
150 | } else {
151 | successFunction(true,text); // this is just the changeset ID
152 | }
153 | })
154 | });
155 | }
156 |
157 | // Upload all dirty objects
158 | uploadChanges(username, password, changesetID, successFunction) { var _this=this;
159 | // Create XML document
160 | var xml = document.implementation.createDocument(null,null);
161 | var osc = xml.createElement("osmChange");
162 | osc.setAttribute('version','0.6');
163 | osc.setAttribute('generator','osmgraph.js');
164 | var create = xml.createElement("create");
165 | var modify = xml.createElement("modify");
166 | // Serialise all changes
167 | var dirty = this.dirtyObjects();
168 | for (var node of dirty.nodes) {
169 | var x = node.toXML(xml,changesetID);
170 | node.id<0 ? create.appendChild(x) : modify.appendChild(x);
171 | }
172 | for (var way of dirty.ways) {
173 | var x = way.toXML(xml,changesetID);
174 | way.id<0 ? create.appendChild(x) : modify.appendChild(x);
175 | }
176 | for (var node of dirty.relations) {
177 | var x = relation.toXML(xml,changesetID);
178 | relation.id<0 ? create.appendChild(x) : modify.appendChild(x);
179 | }
180 | osc.appendChild(create);
181 | osc.appendChild(modify);
182 | xml.appendChild(osc);
183 | // Upload
184 | fetch(this.server+"/api/0.6/changeset/"+changesetID+"/upload", {
185 | method: "POST",
186 | headers: { "Content-Type": "text/xml",
187 | "Authorization": "Basic " + window.btoa(username+":"+password) },
188 | body: new XMLSerializer().serializeToString(xml)
189 | }).then(response => {
190 | if (response.ok) {
191 | response.text()
192 | .then(str => (new window.DOMParser()).parseFromString(str, "text/xml"))
193 | .then(doc => _this.parseDiffResponse(doc))
194 | .then(() => successFunction(true));
195 | } else {
196 | successFunction(false, response);
197 | }
198 | });
199 | }
200 |
201 | // Parse OSM diff response
202 | parseDiffResponse(doc) {
203 | for (var n of doc.getElementsByTagName('node') ) { this.assignNew(n,this.nodes); }
204 | for (var w of doc.getElementsByTagName('way') ) { this.assignNew(w,this.ways); }
205 | for (var r of doc.getElementsByTagName('relation')) { this.assignNew(r,this.relations); }
206 | }
207 | assignNew(el,collection) {
208 | var old_id = Number(el.attributes['old_id'].value);
209 | var new_id = Number(el.attributes['new_id'].value);
210 | if (!collection[old_id]) return;
211 | collection[old_id].id = new_id;
212 | collection[old_id].version = Number(el.attributes['new_version'].value);
213 | collection[old_id].dirty = false;
214 | collection[new_id] = collection[old_id];
215 | delete collection[old_id];
216 | }
217 | }
218 |
219 | class Node {
220 | constructor(id,tags,version,lat,lon) {
221 | this.id = Number(id);
222 | this.tags = tags;
223 | this.version = Number(version);
224 | this.lon = Number(lon);
225 | this.lat = Number(lat);
226 | this.poi = true;
227 | this.dirty = false;
228 | this.parents = [];
229 | }
230 | distanceFrom(latlng) {
231 | return { distance: OSMGraph.fastDistance(latlng.lat,latlng.lng,this.lat,this.lon), lat: this.lat, lon: this.lon }
232 | }
233 | asLeafletHighlight() {
234 | return L.marker([this.lat,this.lon]);
235 | }
236 | parentWaysWithKey(k,graph) {
237 | var parentWays = [];
238 | for (var p of this.parents) {
239 | if (p.constructor.name=='Way' && p.tags[k]) {
240 | parentWays.push(p);
241 | }
242 | }
243 | return parentWays;
244 | }
245 | toXML(xml,changesetID) {
246 | var node = xml.createElement("node");
247 | node.setAttribute("id",this.id);
248 | if (this.id > 0) node.setAttribute("version",this.version);
249 | if (changesetID) node.setAttribute("changeset",changesetID);
250 | node.setAttribute("lat",this.lat);
251 | node.setAttribute("lon",this.lon);
252 | OSMGraph.tagsToXML(this,xml,node);
253 | return node;
254 | }
255 | }
256 | class Way {
257 | constructor(id,tags,version,nodes) {
258 | this.id = Number(id);
259 | this.tags = tags;
260 | this.version = Number(version);
261 | this.nodes = nodes;
262 | this.dirty = false;
263 | this.parents = [];
264 | for (var n of this.nodes) { n.parents.push(this); }
265 | }
266 | distanceFrom(latlng) {
267 | if (this.isClosed() && this.encloses(latlng)) return 0;
268 | var bestDist = Infinity, bestLatLng;
269 | for (var i=0; i 1) { closest = { lat: this.nodes[i+1].lat, lon: this.nodes[i+1].lon }; }
278 | else { closest = { lat: this.nodes[i].lat+u*dx, lon: this.nodes[i].lon+u*dy }; }
279 | var dist = OSMGraph.fastDistance(closest.lat,closest.lon,latlng.lat,latlng.lng);
280 | if (dist y) != (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
308 | if (intersect) inside = !inside;
309 | }
310 | return inside;
311 | }
312 | toXML(xml,changesetID) {
313 | var way = xml.createElement("way");
314 | way.setAttribute("id",this.id);
315 | if (this.id > 0) way.setAttribute("version",this.version);
316 | if (changesetID) way.setAttribute("changeset",changesetID);
317 | OSMGraph.tagsToXML(this,xml,way);
318 | for (var node of this.nodes) {
319 | var nd = xml.createElement("nd");
320 | nd.setAttribute("ref",node.id);
321 | way.appendChild(nd);
322 | }
323 | return way;
324 | }
325 | }
326 | class Relation {
327 | constructor(id,tags,version,members) {
328 | this.id = Number(id);
329 | this.tags = tags;
330 | this.version = Number(version);
331 | this.members = members;
332 | this.dirty = false;
333 | this.parents = [];
334 | for (var m of this.members) { m.obj.parents.push(this); }
335 | }
336 | findOuter() {
337 | if (this.tags['type']!='multipolygon') return null;
338 | for (var m of this.members) {
339 | if (m.role=='outer') return m.obj;
340 | }
341 | return null;
342 | }
343 | // Geometry operators just look at a single outer ring currently
344 | asLeafletHighlight() { var o = this.findOuter(); return o ? o.asLeafletHighlight() : null; }
345 | distanceFrom(latlng) { var o = this.findOuter(); return o ? o.distanceFrom(latlng) : null; }
346 | distanceFromFeature(feature) { var o = this.findOuter(); return o ? o.distanceFromFeature(latlng) : null; }
347 | isClosed() { var o = this.findOuter(); return o ? o.isClosed() : false; }
348 | isArea() { return this.isClosed(); }
349 | encloses(latlng) { var o = this.findOuter(); return o ? o.encloses(latlng) : false; }
350 | toXML(xml,changesetID) {
351 | var rel = xml.createElement("relation");
352 | rel.setAttribute("id",this.id);
353 | if (this.id > 0) rel.setAttribute("version",this.version);
354 | if (changesetID) rel.setAttribute("changeset",changesetID);
355 | OSMGraph.tagsToXML(this,xml,rel);
356 | for (var member of this.members) { rel.appendChild(member.toXML(xml)); }
357 | return rel;
358 | }
359 | }
360 | class RelationMember {
361 | constructor(obj,role) {
362 | this.obj = obj;
363 | this.role = role;
364 | }
365 | toXML(xml) {
366 | var mem = xml.createElement("member");
367 | mem.setAttribute("ref",this.obj.id);
368 | mem.setAttribute("type",this.obj.constructor.name.toLowerCase());
369 | if (this.role) mem.setAttribute("role",this.role);
370 | return mem;
371 | }
372 | }
--------------------------------------------------------------------------------
/static/leaflet/leaflet.css:
--------------------------------------------------------------------------------
1 | /* required styles */
2 |
3 | .leaflet-pane,
4 | .leaflet-tile,
5 | .leaflet-marker-icon,
6 | .leaflet-marker-shadow,
7 | .leaflet-tile-container,
8 | .leaflet-pane > svg,
9 | .leaflet-pane > canvas,
10 | .leaflet-zoom-box,
11 | .leaflet-image-layer,
12 | .leaflet-layer {
13 | position: absolute;
14 | left: 0;
15 | top: 0;
16 | }
17 | .leaflet-container {
18 | overflow: hidden;
19 | }
20 | .leaflet-tile,
21 | .leaflet-marker-icon,
22 | .leaflet-marker-shadow {
23 | -webkit-user-select: none;
24 | -moz-user-select: none;
25 | user-select: none;
26 | -webkit-user-drag: none;
27 | }
28 | /* Safari renders non-retina tile on retina better with this, but Chrome is worse */
29 | .leaflet-safari .leaflet-tile {
30 | image-rendering: -webkit-optimize-contrast;
31 | }
32 | /* hack that prevents hw layers "stretching" when loading new tiles */
33 | .leaflet-safari .leaflet-tile-container {
34 | width: 1600px;
35 | height: 1600px;
36 | -webkit-transform-origin: 0 0;
37 | }
38 | .leaflet-marker-icon,
39 | .leaflet-marker-shadow {
40 | display: block;
41 | }
42 | /* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
43 | /* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
44 | .leaflet-container .leaflet-overlay-pane svg,
45 | .leaflet-container .leaflet-marker-pane img,
46 | .leaflet-container .leaflet-shadow-pane img,
47 | .leaflet-container .leaflet-tile-pane img,
48 | .leaflet-container img.leaflet-image-layer {
49 | max-width: none !important;
50 | }
51 |
52 | .leaflet-container.leaflet-touch-zoom {
53 | -ms-touch-action: pan-x pan-y;
54 | touch-action: pan-x pan-y;
55 | }
56 | .leaflet-container.leaflet-touch-drag {
57 | -ms-touch-action: pinch-zoom;
58 | }
59 | .leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
60 | -ms-touch-action: none;
61 | touch-action: none;
62 | }
63 | .leaflet-container {
64 | -webkit-tap-highlight-color: transparent;
65 | }
66 | .leaflet-container a {
67 | -webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
68 | }
69 | .leaflet-tile {
70 | filter: inherit;
71 | visibility: hidden;
72 | }
73 | .leaflet-tile-loaded {
74 | visibility: inherit;
75 | }
76 | .leaflet-zoom-box {
77 | width: 0;
78 | height: 0;
79 | -moz-box-sizing: border-box;
80 | box-sizing: border-box;
81 | z-index: 800;
82 | }
83 | /* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
84 | .leaflet-overlay-pane svg {
85 | -moz-user-select: none;
86 | }
87 |
88 | .leaflet-pane { z-index: 400; }
89 |
90 | .leaflet-tile-pane { z-index: 200; }
91 | .leaflet-overlay-pane { z-index: 400; }
92 | .leaflet-shadow-pane { z-index: 500; }
93 | .leaflet-marker-pane { z-index: 600; }
94 | .leaflet-tooltip-pane { z-index: 650; }
95 | .leaflet-popup-pane { z-index: 700; }
96 |
97 | .leaflet-map-pane canvas { z-index: 100; }
98 | .leaflet-map-pane svg { z-index: 200; }
99 |
100 | .leaflet-vml-shape {
101 | width: 1px;
102 | height: 1px;
103 | }
104 | .lvml {
105 | behavior: url(#default#VML);
106 | display: inline-block;
107 | position: absolute;
108 | }
109 |
110 |
111 | /* control positioning */
112 |
113 | .leaflet-control {
114 | position: relative;
115 | z-index: 800;
116 | pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
117 | pointer-events: auto;
118 | }
119 | .leaflet-top,
120 | .leaflet-bottom {
121 | position: absolute;
122 | z-index: 1000;
123 | pointer-events: none;
124 | }
125 | .leaflet-top {
126 | top: 0;
127 | }
128 | .leaflet-right {
129 | right: 0;
130 | }
131 | .leaflet-bottom {
132 | bottom: 0;
133 | }
134 | .leaflet-left {
135 | left: 0;
136 | }
137 | .leaflet-control {
138 | float: left;
139 | clear: both;
140 | }
141 | .leaflet-right .leaflet-control {
142 | float: right;
143 | }
144 | .leaflet-top .leaflet-control {
145 | margin-top: 10px;
146 | }
147 | .leaflet-bottom .leaflet-control {
148 | margin-bottom: 10px;
149 | }
150 | .leaflet-left .leaflet-control {
151 | margin-left: 10px;
152 | }
153 | .leaflet-right .leaflet-control {
154 | margin-right: 10px;
155 | }
156 |
157 |
158 | /* zoom and fade animations */
159 |
160 | .leaflet-fade-anim .leaflet-tile {
161 | will-change: opacity;
162 | }
163 | .leaflet-fade-anim .leaflet-popup {
164 | opacity: 0;
165 | -webkit-transition: opacity 0.2s linear;
166 | -moz-transition: opacity 0.2s linear;
167 | -o-transition: opacity 0.2s linear;
168 | transition: opacity 0.2s linear;
169 | }
170 | .leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
171 | opacity: 1;
172 | }
173 | .leaflet-zoom-animated {
174 | -webkit-transform-origin: 0 0;
175 | -ms-transform-origin: 0 0;
176 | transform-origin: 0 0;
177 | }
178 | .leaflet-zoom-anim .leaflet-zoom-animated {
179 | will-change: transform;
180 | }
181 | .leaflet-zoom-anim .leaflet-zoom-animated {
182 | -webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
183 | -moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
184 | -o-transition: -o-transform 0.25s cubic-bezier(0,0,0.25,1);
185 | transition: transform 0.25s cubic-bezier(0,0,0.25,1);
186 | }
187 | .leaflet-zoom-anim .leaflet-tile,
188 | .leaflet-pan-anim .leaflet-tile {
189 | -webkit-transition: none;
190 | -moz-transition: none;
191 | -o-transition: none;
192 | transition: none;
193 | }
194 |
195 | .leaflet-zoom-anim .leaflet-zoom-hide {
196 | visibility: hidden;
197 | }
198 |
199 |
200 | /* cursors */
201 |
202 | .leaflet-interactive {
203 | cursor: pointer;
204 | }
205 | .leaflet-grab {
206 | cursor: -webkit-grab;
207 | cursor: -moz-grab;
208 | }
209 | .leaflet-crosshair,
210 | .leaflet-crosshair .leaflet-interactive {
211 | cursor: crosshair;
212 | }
213 | .leaflet-popup-pane,
214 | .leaflet-control {
215 | cursor: auto;
216 | }
217 | .leaflet-dragging .leaflet-grab,
218 | .leaflet-dragging .leaflet-grab .leaflet-interactive,
219 | .leaflet-dragging .leaflet-marker-draggable {
220 | cursor: move;
221 | cursor: -webkit-grabbing;
222 | cursor: -moz-grabbing;
223 | }
224 |
225 | /* marker & overlays interactivity */
226 | .leaflet-marker-icon,
227 | .leaflet-marker-shadow,
228 | .leaflet-image-layer,
229 | .leaflet-pane > svg path,
230 | .leaflet-tile-container {
231 | pointer-events: none;
232 | }
233 |
234 | .leaflet-marker-icon.leaflet-interactive,
235 | .leaflet-image-layer.leaflet-interactive,
236 | .leaflet-pane > svg path.leaflet-interactive {
237 | pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
238 | pointer-events: auto;
239 | }
240 |
241 | /* visual tweaks */
242 |
243 | .leaflet-container {
244 | background: #ddd;
245 | outline: 0;
246 | }
247 | .leaflet-container a {
248 | color: #0078A8;
249 | }
250 | .leaflet-container a.leaflet-active {
251 | outline: 2px solid orange;
252 | }
253 | .leaflet-zoom-box {
254 | border: 2px dotted #38f;
255 | background: rgba(255,255,255,0.5);
256 | }
257 |
258 |
259 | /* general typography */
260 | .leaflet-container {
261 | font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif;
262 | }
263 |
264 |
265 | /* general toolbar styles */
266 |
267 | .leaflet-bar {
268 | box-shadow: 0 1px 5px rgba(0,0,0,0.65);
269 | border-radius: 4px;
270 | }
271 | .leaflet-bar a,
272 | .leaflet-bar a:hover {
273 | background-color: #fff;
274 | border-bottom: 1px solid #ccc;
275 | width: 26px;
276 | height: 26px;
277 | line-height: 26px;
278 | display: block;
279 | text-align: center;
280 | text-decoration: none;
281 | color: black;
282 | }
283 | .leaflet-bar a,
284 | .leaflet-control-layers-toggle {
285 | background-position: 50% 50%;
286 | background-repeat: no-repeat;
287 | display: block;
288 | }
289 | .leaflet-bar a:hover {
290 | background-color: #f4f4f4;
291 | }
292 | .leaflet-bar a:first-child {
293 | border-top-left-radius: 4px;
294 | border-top-right-radius: 4px;
295 | }
296 | .leaflet-bar a:last-child {
297 | border-bottom-left-radius: 4px;
298 | border-bottom-right-radius: 4px;
299 | border-bottom: none;
300 | }
301 | .leaflet-bar a.leaflet-disabled {
302 | cursor: default;
303 | background-color: #f4f4f4;
304 | color: #bbb;
305 | }
306 |
307 | .leaflet-touch .leaflet-bar a {
308 | width: 30px;
309 | height: 30px;
310 | line-height: 30px;
311 | }
312 | .leaflet-touch .leaflet-bar a:first-child {
313 | border-top-left-radius: 2px;
314 | border-top-right-radius: 2px;
315 | }
316 | .leaflet-touch .leaflet-bar a:last-child {
317 | border-bottom-left-radius: 2px;
318 | border-bottom-right-radius: 2px;
319 | }
320 |
321 | /* zoom control */
322 |
323 | .leaflet-control-zoom-in,
324 | .leaflet-control-zoom-out {
325 | font: bold 18px 'Lucida Console', Monaco, monospace;
326 | text-indent: 1px;
327 | }
328 |
329 | .leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
330 | font-size: 22px;
331 | }
332 |
333 |
334 | /* layers control */
335 |
336 | .leaflet-control-layers {
337 | box-shadow: 0 1px 5px rgba(0,0,0,0.4);
338 | background: #fff;
339 | border-radius: 5px;
340 | }
341 | .leaflet-control-layers-toggle {
342 | background-image: url(images/layers.png);
343 | width: 36px;
344 | height: 36px;
345 | }
346 | .leaflet-retina .leaflet-control-layers-toggle {
347 | background-image: url(images/layers-2x.png);
348 | background-size: 26px 26px;
349 | }
350 | .leaflet-touch .leaflet-control-layers-toggle {
351 | width: 44px;
352 | height: 44px;
353 | }
354 | .leaflet-control-layers .leaflet-control-layers-list,
355 | .leaflet-control-layers-expanded .leaflet-control-layers-toggle {
356 | display: none;
357 | }
358 | .leaflet-control-layers-expanded .leaflet-control-layers-list {
359 | display: block;
360 | position: relative;
361 | }
362 | .leaflet-control-layers-expanded {
363 | padding: 6px 10px 6px 6px;
364 | color: #333;
365 | background: #fff;
366 | }
367 | .leaflet-control-layers-scrollbar {
368 | overflow-y: scroll;
369 | overflow-x: hidden;
370 | padding-right: 5px;
371 | }
372 | .leaflet-control-layers-selector {
373 | margin-top: 2px;
374 | position: relative;
375 | top: 1px;
376 | }
377 | .leaflet-control-layers label {
378 | display: block;
379 | }
380 | .leaflet-control-layers-separator {
381 | height: 0;
382 | border-top: 1px solid #ddd;
383 | margin: 5px -10px 5px -6px;
384 | }
385 |
386 | /* Default icon URLs */
387 | .leaflet-default-icon-path {
388 | background-image: url(images/marker-icon.png);
389 | }
390 |
391 |
392 | /* attribution and scale controls */
393 |
394 | .leaflet-container .leaflet-control-attribution {
395 | background: #fff;
396 | background: rgba(255, 255, 255, 0.7);
397 | margin: 0;
398 | }
399 | .leaflet-control-attribution,
400 | .leaflet-control-scale-line {
401 | padding: 0 5px;
402 | color: #333;
403 | }
404 | .leaflet-control-attribution a {
405 | text-decoration: none;
406 | }
407 | .leaflet-control-attribution a:hover {
408 | text-decoration: underline;
409 | }
410 | .leaflet-container .leaflet-control-attribution,
411 | .leaflet-container .leaflet-control-scale {
412 | font-size: 11px;
413 | }
414 | .leaflet-left .leaflet-control-scale {
415 | margin-left: 5px;
416 | }
417 | .leaflet-bottom .leaflet-control-scale {
418 | margin-bottom: 5px;
419 | }
420 | .leaflet-control-scale-line {
421 | border: 2px solid #777;
422 | border-top: none;
423 | line-height: 1.1;
424 | padding: 2px 5px 1px;
425 | font-size: 11px;
426 | white-space: nowrap;
427 | overflow: hidden;
428 | -moz-box-sizing: border-box;
429 | box-sizing: border-box;
430 |
431 | background: #fff;
432 | background: rgba(255, 255, 255, 0.5);
433 | }
434 | .leaflet-control-scale-line:not(:first-child) {
435 | border-top: 2px solid #777;
436 | border-bottom: none;
437 | margin-top: -2px;
438 | }
439 | .leaflet-control-scale-line:not(:first-child):not(:last-child) {
440 | border-bottom: 2px solid #777;
441 | }
442 |
443 | .leaflet-touch .leaflet-control-attribution,
444 | .leaflet-touch .leaflet-control-layers,
445 | .leaflet-touch .leaflet-bar {
446 | box-shadow: none;
447 | }
448 | .leaflet-touch .leaflet-control-layers,
449 | .leaflet-touch .leaflet-bar {
450 | border: 2px solid rgba(0,0,0,0.2);
451 | background-clip: padding-box;
452 | }
453 |
454 |
455 | /* popup */
456 |
457 | .leaflet-popup {
458 | position: absolute;
459 | text-align: center;
460 | margin-bottom: 20px;
461 | }
462 | .leaflet-popup-content-wrapper {
463 | padding: 1px;
464 | text-align: left;
465 | border-radius: 12px;
466 | }
467 | .leaflet-popup-content {
468 | margin: 13px 19px;
469 | line-height: 1.4;
470 | }
471 | .leaflet-popup-content p {
472 | margin: 18px 0;
473 | }
474 | .leaflet-popup-tip-container {
475 | width: 40px;
476 | height: 20px;
477 | position: absolute;
478 | left: 50%;
479 | margin-left: -20px;
480 | overflow: hidden;
481 | pointer-events: none;
482 | }
483 | .leaflet-popup-tip {
484 | width: 17px;
485 | height: 17px;
486 | padding: 1px;
487 |
488 | margin: -10px auto 0;
489 |
490 | -webkit-transform: rotate(45deg);
491 | -moz-transform: rotate(45deg);
492 | -ms-transform: rotate(45deg);
493 | -o-transform: rotate(45deg);
494 | transform: rotate(45deg);
495 | }
496 | .leaflet-popup-content-wrapper,
497 | .leaflet-popup-tip {
498 | background: white;
499 | color: #333;
500 | box-shadow: 0 3px 14px rgba(0,0,0,0.4);
501 | }
502 | .leaflet-container a.leaflet-popup-close-button {
503 | position: absolute;
504 | top: 0;
505 | right: 0;
506 | padding: 4px 4px 0 0;
507 | border: none;
508 | text-align: center;
509 | width: 18px;
510 | height: 14px;
511 | font: 16px/14px Tahoma, Verdana, sans-serif;
512 | color: #c3c3c3;
513 | text-decoration: none;
514 | font-weight: bold;
515 | background: transparent;
516 | }
517 | .leaflet-container a.leaflet-popup-close-button:hover {
518 | color: #999;
519 | }
520 | .leaflet-popup-scrolled {
521 | overflow: auto;
522 | border-bottom: 1px solid #ddd;
523 | border-top: 1px solid #ddd;
524 | }
525 |
526 | .leaflet-oldie .leaflet-popup-content-wrapper {
527 | zoom: 1;
528 | }
529 | .leaflet-oldie .leaflet-popup-tip {
530 | width: 24px;
531 | margin: 0 auto;
532 |
533 | -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
534 | filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
535 | }
536 | .leaflet-oldie .leaflet-popup-tip-container {
537 | margin-top: -1px;
538 | }
539 |
540 | .leaflet-oldie .leaflet-control-zoom,
541 | .leaflet-oldie .leaflet-control-layers,
542 | .leaflet-oldie .leaflet-popup-content-wrapper,
543 | .leaflet-oldie .leaflet-popup-tip {
544 | border: 1px solid #999;
545 | }
546 |
547 |
548 | /* div icon */
549 |
550 | .leaflet-div-icon {
551 | background: #fff;
552 | border: 1px solid #666;
553 | }
554 |
555 |
556 | /* Tooltip */
557 | /* Base styles for the element that has a tooltip */
558 | .leaflet-tooltip {
559 | position: absolute;
560 | padding: 6px;
561 | background-color: #fff;
562 | border: 1px solid #fff;
563 | border-radius: 3px;
564 | color: #222;
565 | white-space: nowrap;
566 | -webkit-user-select: none;
567 | -moz-user-select: none;
568 | -ms-user-select: none;
569 | user-select: none;
570 | pointer-events: none;
571 | box-shadow: 0 1px 3px rgba(0,0,0,0.4);
572 | }
573 | .leaflet-tooltip.leaflet-clickable {
574 | cursor: pointer;
575 | pointer-events: auto;
576 | }
577 | .leaflet-tooltip-top:before,
578 | .leaflet-tooltip-bottom:before,
579 | .leaflet-tooltip-left:before,
580 | .leaflet-tooltip-right:before {
581 | position: absolute;
582 | pointer-events: none;
583 | border: 6px solid transparent;
584 | background: transparent;
585 | content: "";
586 | }
587 |
588 | /* Directions */
589 |
590 | .leaflet-tooltip-bottom {
591 | margin-top: 6px;
592 | }
593 | .leaflet-tooltip-top {
594 | margin-top: -6px;
595 | }
596 | .leaflet-tooltip-bottom:before,
597 | .leaflet-tooltip-top:before {
598 | left: 50%;
599 | margin-left: -6px;
600 | }
601 | .leaflet-tooltip-top:before {
602 | bottom: 0;
603 | margin-bottom: -12px;
604 | border-top-color: #fff;
605 | }
606 | .leaflet-tooltip-bottom:before {
607 | top: 0;
608 | margin-top: -12px;
609 | margin-left: -6px;
610 | border-bottom-color: #fff;
611 | }
612 | .leaflet-tooltip-left {
613 | margin-left: -6px;
614 | }
615 | .leaflet-tooltip-right {
616 | margin-left: 6px;
617 | }
618 | .leaflet-tooltip-left:before,
619 | .leaflet-tooltip-right:before {
620 | top: 50%;
621 | margin-top: -6px;
622 | }
623 | .leaflet-tooltip-left:before {
624 | right: 0;
625 | margin-right: -12px;
626 | border-left-color: #fff;
627 | }
628 | .leaflet-tooltip-right:before {
629 | left: 0;
630 | margin-left: -12px;
631 | border-right-color: #fff;
632 | }
633 |
--------------------------------------------------------------------------------
/static/conflation-browser.js:
--------------------------------------------------------------------------------
1 |
2 | // Globals
3 |
4 | var glMap, style={}, graph, popup, proposedEdits, displayedEdit, leafletMap, leafletFeature, leafletCandidate, selectedFeature, changesetID;
5 | var TOP_LEVEL = ["aeroway","amenity","barrier","boundary","building","emergency","entrance","highway","historic","landuse",
6 | "leisure", "man_made", "natural", "office", "place", "power", "public_transport", "railway", "route", "shop", "traffic_sign",
7 | "tourism", "waterway"];
8 |
9 | // Begin by fetching metadata
10 |
11 | function fetchMetadata() {
12 | fetch("/metadata")
13 | .then(function(resp) { return resp.json(); })
14 | .then(initialiseMap)
15 | .catch(mapError);
16 | }
17 |
18 | // Initialise map
19 |
20 | function initialiseMap(metadata) {
21 | graph = new OSMGraph("https://www.openstreetmap.org");
22 | // graph = new OSMGraph("https://master.apis.dev.openstreetmap.org"); // for testing
23 | var bbox = metadata.bounds.split(',').map(x=>Number(x));
24 |
25 | // Auto-generate style
26 | // for ESRI Clarity background: https://clarity.maptiles.arcgis.com/arcgis/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}
27 | // for OSM background: or http://tile.openstreetmap.org/{z}/{x}/{y}.png
28 | style = {
29 | "version": 8,
30 | "name": "conflation default",
31 | "sources": {
32 | "raster": {
33 | "type": "raster",
34 | "tiles": ["https://clarity.maptiles.arcgis.com/arcgis/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"],
35 | "scheme": "xyz"
36 | },
37 | "conflation": {
38 | "type": "vector",
39 | "tiles": [window.location.protocol+"//"+window.location.host+"/{z}/{x}/{y}.pbf"],
40 | "minzoom": Number(metadata.minzoom),
41 | "maxzoom": Number(metadata.maxzoom),
42 | "scheme": "xyz"
43 | }
44 | },
45 | "layers": [{
46 | "id": "raster",
47 | "type": "raster",
48 | "source": "raster",
49 | "minzoom": 0,
50 | "maxzoom": 22
51 | }]
52 | };
53 | var polygonLayers=[], pointLayers=[], polylineLayers=[];
54 | for (const src of metadata.json.vector_layers) {
55 | var base = {
56 | "source": "conflation",
57 | "source-layer": src.id,
58 | "minzoom": src.minzoom,
59 | "maxzoom": 22
60 | };
61 | var polygonLayer = {
62 | "id": src.id+"_poly",
63 | "type": "fill",
64 | "filter": ["all",
65 | ["match", ["geometry-type"], ["Polygon", "MultiPolygon"], true, false],
66 | ["match", ["get","id"], ["ZZZ"], false, true]
67 | ]
68 | // layout, paint
69 | };
70 | var pointLayer = {
71 | "id": src.id+"_point",
72 | "type": "circle",
73 | "filter": ["all",
74 | ["match", ["geometry-type"], ["Point", "MultiPoint"], true, false],
75 | ["match", ["get","id"], ["ZZZ"], false, true]
76 | ],
77 | "paint": { "circle-color": "#FF0000", "circle-radius": 8 }
78 | };
79 | var polylineLayer = {
80 | "id": src.id+"_line",
81 | "type": "line",
82 | "paint": {"line-color":"#4444ee","line-width":3},
83 | "filter": ["all",
84 | ["match", ["geometry-type"], ["LineString", "MultiLineString"], true, false],
85 | ["match", ["get","id"], ["ZZZ"], false, true]
86 | ],
87 | "paint": { "line-color": "#00FFFF", "line-width": 3 }
88 | };
89 | Object.assign(polygonLayer, base); polygonLayers.push(polygonLayer);
90 | Object.assign(pointLayer, base); pointLayers.push(pointLayer);
91 | Object.assign(polylineLayer, base); polylineLayers.push(polylineLayer);
92 | }
93 | style.layers = style.layers.concat(polygonLayers, polylineLayers, pointLayers);
94 |
95 | var centre = [(bbox[0]+bbox[2])/2, (bbox[1]+bbox[3])/2];
96 | glMap = new mapboxgl.Map({
97 | container: 'map',
98 | style: style,
99 | center: centre,
100 | zoom: 14
101 | });
102 | // glMap.showTileBoundaries=true; // for tile debugging
103 | glMap.addControl(new mapboxgl.NavigationControl());
104 |
105 | glMap.on('click', featureClicked);
106 | glMap.on('move', mapMoved);
107 |
108 | // Initialise Leaflet map
109 | leafletMap = L.map('leaflet').setView([centre[1],centre[0]],14);
110 | var osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
111 | attribution: "© OpenStreetMap contributors | Satellite imagery ©ESRI",
112 | maxNativeZoom: 19,
113 | maxZoom: 22 }).addTo(leafletMap);
114 |
115 | // Set up keyboard listener for fast edits
116 | document.addEventListener("keydown", keyListener);
117 |
118 | // Initialise UI
119 | clearProposedEdits();
120 | }
121 |
122 | // ============================================================================================================================================
123 | // Clicking on a feature for an edit
124 |
125 | // Clicked a feature so find nearby features
126 | function featureClicked(e) {
127 | if (popup) popup.remove();
128 | if (leafletFeature) { leafletMap.removeLayer(leafletFeature); leafletFeature = null; }
129 |
130 | // Look for features around the point
131 | var bbox = [[e.point.x - 5, e.point.y - 5], [e.point.x + 5, e.point.y + 5]];
132 | var features = glMap.queryRenderedFeatures(bbox) //, { layers: ['counties'] });
133 |
134 | // Choose a feature, preferring points
135 | if (features.length==0) return;
136 | var feature = null;
137 | for (var f of features) {
138 | if (f.layer.type=='circle') { feature=f; break; }
139 | }
140 | feature = feature || features[0];
141 | selectedFeature = feature;
142 |
143 | // Show popup
144 | var html="";
145 | for (var k in feature.properties) {
146 | if (k=='id') continue;
147 | html += k+"="+feature.properties[k]+"
";
148 | }
149 | html+="
";
150 |
151 | popup = new mapboxgl.Popup()
152 | .setLngLat(glMap.unproject(e.point))
153 | .setHTML(html)
154 | .addTo(glMap);
155 | fetchOSMAPI(feature, glMap.unproject(e.point));
156 |
157 | // Highlight feature on Leaflet map too
158 | var geom = JSON.parse(JSON.stringify(feature.geometry));
159 | if (feature.layer.type=='circle') {
160 | leafletFeature = L.circle(geom.coordinates.reverse(), { radius: 50, fillColor: "#FF0000", fillOpacity: 0.2, stroke: false } ).addTo(leafletMap);
161 | } else if (feature.layer.type=='line') {
162 | leafletFeature = L.polyline(geom.coordinates.map(pt => pt.reverse()), { color: "#00FFFF", weight: 15, opacity: 0.5 } ).addTo(leafletMap);
163 | }
164 | if (leafletFeature) {
165 | var b1 = leafletFeature.getBounds();
166 | var b2 = leafletMap.getBounds();
167 | if (!b1.intersects(b2)) {
168 | leafletMap.fitBounds(b1, { maxZoom: leafletMap.getZoom() });
169 | }
170 | }
171 | }
172 |
173 | // Fetch OSM data near the selected feature
174 | function fetchOSMAPI(feature, latlng) {
175 | var left = latlng.lng - 0.001;
176 | var right = latlng.lng + 0.001;
177 | var bottom= latlng.lat - 0.001;
178 | var top = latlng.lat + 0.001;
179 | var url = graph.server+"/api/0.6/map?bbox="+([left,bottom,right,top].join(','));
180 | fetch(url)
181 | .then(response => response.text())
182 | .then(str => (new window.DOMParser()).parseFromString(str, "text/xml"))
183 | .then(doc => parseOSMData(feature, latlng, doc));
184 | }
185 |
186 | // Parse OSM data and find the most likely candidates for our suggested feature
187 | function parseOSMData(feature,latlng,doc) {
188 | graph.parseFromXML(doc);
189 | graph.setPOIFlag();
190 | var candidates = findCandidates(feature,latlng,graph);
191 | proposedEdits = [];
192 | var existingMatchFound = false;
193 | for (var candidate of candidates) {
194 | var edit = calculateEditFor(candidate, feature);
195 | if (edit.action=='match') {
196 | // only add one 'match' object
197 | if (existingMatchFound) continue;
198 | existingMatchFound = true;
199 | }
200 | if (edit) proposedEdits.push(edit);
201 | }
202 | var edit = calculateNewObjectFor(feature,latlng);
203 | if (edit) proposedEdits.push(edit);
204 | displayedEdit = 0;
205 | if (proposedEdits.length>0) { renderProposedEdit(); } else { clearProposedEdits(); }
206 | }
207 |
208 | // Assemble the suggested edit for an object
209 | // **** should be a bit smarter - e.g. if candidate has highway=cycleway and feature is highway=primary, then we want to add cycleway=track
210 | function calculateEditFor(candidate, feature) {
211 | var ch=0, tags = {};
212 | for (var k in feature.properties) {
213 | if (candidate.obj.tags[k] != feature.properties[k]) {
214 | if (k!='id' && k[0]!='_') {
215 | tags[k] = feature.properties[k];
216 | ch++;
217 | }
218 | }
219 | }
220 | if (ch==0) return { action: 'match', obj: candidate.obj, tags: {} };
221 | return { action: 'modify', obj: candidate.obj, tags: tags };
222 | }
223 |
224 | // Assemble a new suggested feature
225 | function calculateNewObjectFor(feature, latlng) {
226 | var ch=0, tags = JSON.parse(JSON.stringify(feature.properties));
227 | for (var k in tags) { if (k=='id' || k[0]=='_') { delete tags[k]; } else { ch++; } }
228 | if (ch==0) return null;
229 | var geom = JSON.parse(JSON.stringify(feature.geometry));
230 | if (feature.layer.type=='circle') {
231 | return { action: 'create', type: 'Node', geometry: geom, tags: tags }
232 | } else {
233 | return { action: 'create', type: 'Way', geometry: geom, tags: tags }
234 | }
235 | }
236 |
237 | // Render a proposed edit
238 | function renderProposedEdit() {
239 | var edit = proposedEdits[displayedEdit];
240 | byId('proposedIndex').innerHTML = (displayedEdit+1);
241 | byId('proposedCount').innerHTML = proposedEdits.length;
242 |
243 | // Render a Leaflet object
244 | if (leafletCandidate) { leafletMap.removeLayer(leafletCandidate); leafletCandidate=null; }
245 | if (edit.action=='modify' || edit.action=='match') {
246 | leafletCandidate = edit.obj.asLeafletHighlight();
247 | } else if (edit.type=='Node') {
248 | leafletCandidate = L.marker(edit.geometry.coordinates.reverse());
249 | } else if (edit.type=='Way') {
250 | leafletCandidate = L.polyline(edit.geometry.coordinates.map(pt => pt.reverse()));
251 | } else {
252 | console.log("Unrecognised edit",edit);
253 | }
254 | leafletCandidate.addTo(leafletMap);
255 |
256 | // Assemble a textual list and put it the "proposed" pane
257 | var changes = [];
258 | if (edit.action=='create') {
259 | changes.push({ name: null, description: "Create new "+edit.type });
260 | } else {
261 | var desc = edit.obj.constructor.name + " " + edit.obj.id +
262 | " ⧉";
263 | if (edit.action=='modify') {
264 | changes.push({ name: null, description: "Modify "+desc });
265 | } else {
266 | changes.push({ name: null, description: "Already matches "+desc });
267 | }
268 | }
269 | for (var k in edit.tags) {
270 | if (edit.action=='create' || !edit.obj.tags[k]) {
271 | changes.push({ name: k, description: "Add tag "+k+"="+edit.tags[k] });
272 | } else {
273 | changes.push({ name: k, description: "Change tag "+k+" to "+edit.tags[k]+" (from "+edit.obj.tags[k]+")" })
274 | }
275 | }
276 | var ct=0;
277 | byId('changes').innerHTML = "" + changes.map(function(c) {
278 | if (c.name) {
279 | ct++;
280 | return ""+c.description+" "+String.fromCharCode(9311+ct)+"
";
281 | } else {
282 | return c.description+"
";
283 | }
284 | }).join('') + "
";
285 | byId('nextButton').disabled = byId('ignoreButton').disabled = false;
286 | byId('acceptButton').disabled = byId('acceptAndKeepButton').disabled = (edit.action=='match');
287 | }
288 |
289 | // Clear proposed edit area
290 | function clearProposedEdits() {
291 | byId('changes').innerHTML='';
292 | byId('proposedIndex').innerHTML = '-';
293 | byId('proposedCount').innerHTML = '-';
294 | byId('nextButton').disabled = byId('acceptButton').disabled = byId('acceptAndKeepButton').disabled = byId('ignoreButton').disabled = true;
295 | }
296 |
297 | function nextProposed() {
298 | if (!proposedEdits) return;
299 | displayedEdit = (displayedEdit+1) % proposedEdits.length;
300 | renderProposedEdit();
301 | }
302 |
303 | // ============================================================================================================================================
304 | // Accept/reject changes
305 |
306 | function acceptProposed(keep) {
307 | if (!selectedFeature) return;
308 | if (popup) { popup.remove(); popup=null; }
309 | if (leafletFeature) { leafletMap.removeLayer(leafletFeature); leafletFeature = null; }
310 | if (leafletCandidate) { leafletMap.removeLayer(leafletCandidate); leafletCandidate = null; }
311 | if (!keep) { hideFeature(selectedFeature.properties.id); }
312 | byId('editCount').innerHTML = Number(byId('editCount').innerHTML)+1;
313 | applyChange(proposedEdits[displayedEdit]);
314 | clearProposedEdits();
315 | }
316 | function applyChange(change) {
317 | var tags = {};
318 | for (var k in change.tags) {
319 | var el = document.querySelectorAll("input.tag_change[name="+k+"]")[0];
320 | if (el.checked) { tags[k] = change.tags[k]; }
321 | }
322 |
323 | if (change.action=='modify') {
324 | // modify
325 | for (var k in tags) { change.obj.tags[k] = tags[k]; }
326 | change.obj.dirty = true;
327 |
328 | } else if (change.type=='Way') {
329 | // create way
330 | // **** could potentially Douglas-Peucker it
331 | var nodeIndex = {}, nodeList = [];
332 | for (var i=0; i console.log(response.status+": "+text));
379 | } else {
380 | alert("Your changes were successfully uploaded!");
381 | console.log(response);
382 | byId('editCount').innerHTML = "0";
383 | }
384 | }
385 |
386 | // ============================================================================================================================================
387 | // Matching
388 |
389 | // Find likely candidates
390 | function findCandidates(feature,latlng,graph) {
391 | var sets; // should we look through nodes, ways, relations?
392 | var filter; // filter function to get the correct type (e.g. only POI nodes, or only closed ways)
393 | if (feature.properties['_filter']) {
394 | // if we have an explicit filter type, use that
395 | // **** only waynode:(tag) supported at present
396 | var wayKey = feature.properties['_filter'].split(':')[1];
397 | sets = [graph.nodes];
398 | filter = function(obj) {
399 | return !obj.poi && obj.parentWaysWithKey(wayKey,graph).length>0;
400 | }
401 | } else if (feature.layer.type=='circle') {
402 | // if it's a circle, look for POI nodes or closed ways
403 | sets = [graph.nodes, graph.ways, graph.relations];
404 | filter = function(obj) {
405 | if (obj.constructor.name=='Node') {
406 | return obj.poi;
407 | } else {
408 | return obj.isArea();
409 | }
410 | }
411 | } else if (feature.layer.type=='line') {
412 | // if it's a line, look for unclosed ways
413 | sets = [graph.ways];
414 | filter = function(obj) { return !obj.isArea(); }
415 |
416 | } else if (feature.layer.type=='fill') {
417 | // if it's a polygon, look for closed ways/multipolygons
418 | sets = [graph.ways, graph.relations];
419 | filter = function(obj) { return obj.isArea(); }
420 | }
421 |
422 | var candidates = [];
423 | for (var s of sets) {
424 | for (var id in s) {
425 | var obj = s[id];
426 | if (obj.dirty) continue; // don't suggest anything already changed
427 | if (!filter(obj)) continue; // filter on type
428 | var c = compatible(feature,obj.tags); if (c==0) continue; // filter on tags
429 | var d = obj.distanceFrom(latlng); if (d.distance>150) continue; // filter on distance
430 | // **** if it's a way, we probably want to filter on "distance between polylines", not just from the clickpoint
431 | // (obviously a bit tricky because the way and feature will have different extents)
432 | candidates.push( { distance: d.distance, score: c, obj: obj });
433 | }
434 | }
435 | candidates.sort(function(a,b) { return cmp( Math.sqrt(a.distance) + 10*(3-a.score),
436 | Math.sqrt(b.distance) + 10*(3-b.score) ) });
437 | return candidates;
438 | }
439 | function cmp(a,b) {
440 | if (a > b) return +1;
441 | if (a < b) return -1;
442 | return 0;
443 | }
444 |
445 | // Compare tags
446 | // score 0 for no match, 1 for top-level key match, 2 for top-level key match with same value
447 | function compatible(feature,tags) {
448 | var score = 0;
449 | for (var k of TOP_LEVEL) {
450 | if (feature.properties[k] && tags[k]) {
451 | score = Math.max(feature.properties[k]==tags[k] ? 3 : 1, score);
452 | }
453 | }
454 | // _match_key allows us to look for a particular key
455 | var mk = feature.properties['_match_key'];
456 | if (tags[mk] || (mk=="" && !tags[mk])) { score = Math.max(1,score); }
457 | // custom matches
458 | // highway=path/footway/cycleway equivalent
459 | if (feature.properties['highway']=='path' && (tags['highway']=='cycleway' || tags['highway']=='footway')) { score=2; }
460 | // cycleway= implies highway=
461 | if (feature.properties['cycleway'] && tags['highway']) { score=1.5; }
462 | // **** could add lots more here
463 | return score;
464 | }
465 |
466 | // ============================================================================================================================================
467 | // Keyboard listener for fast edits
468 |
469 | function keyListener(event) {
470 | if (document.activeElement.nodeName=="INPUT") return; // don't hijack text entry
471 | if (event.key=="Enter") {
472 | // accept change
473 | acceptProposed();
474 | } else if (event.key=="Backspace") {
475 | // ignore change
476 | ignoreProposed();
477 | } else if (event.key==" ") {
478 | // next candidate
479 | nextProposed();
480 | event.stopImmediatePropagation();
481 | } else if (event.keyCode>=49 && event.keyCode<=57) {
482 | // toggle checkbox
483 | var el = byId("toggle"+event.key);
484 | if (el) el.checked=!el.checked;
485 | }
486 | }
487 |
488 | // ============================================================================================================================================
489 | // Support code
490 |
491 | // Hide feature
492 |
493 | function hideFeature(id) {
494 | for (var layer of glMap.getStyle().layers) {
495 | if (!layer.filter) continue;
496 | if (layer.filter[2][2].indexOf(id)>-1) continue;
497 | var f = JSON.parse(JSON.stringify(layer.filter));
498 | f[2][2].push(id);
499 | glMap.setFilter(layer.id,f);
500 | }
501 | }
502 |
503 | // Leaflet map interaction
504 |
505 | function mapMoved(event) {
506 | var ll = glMap.getCenter();
507 | leafletMap.panTo(ll, { animate: false });
508 | }
509 |
510 | // Debug etc.
511 |
512 | function mapError(err) {
513 | console.log("Error",err);
514 | }
515 |
516 | function byId(id) { return document.getElementById(id); }
517 |
--------------------------------------------------------------------------------
/static/mbgl/mapbox-gl.css:
--------------------------------------------------------------------------------
1 | .mapboxgl-map {
2 | font: 12px/20px 'Helvetica Neue', Arial, Helvetica, sans-serif;
3 | overflow: hidden;
4 | position: relative;
5 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
6 | }
7 |
8 | .mapboxgl-map:-webkit-full-screen {
9 | width: 100%;
10 | height: 100%;
11 | }
12 |
13 | .mapboxgl-canary {
14 | background-color: salmon;
15 | }
16 |
17 | .mapboxgl-canvas-container.mapboxgl-interactive,
18 | .mapboxgl-ctrl-group > button.mapboxgl-ctrl-compass {
19 | cursor: -webkit-grab;
20 | cursor: -moz-grab;
21 | cursor: grab;
22 | -moz-user-select: none;
23 | -webkit-user-select: none;
24 | -ms-user-select: none;
25 | user-select: none;
26 | }
27 |
28 | .mapboxgl-canvas-container.mapboxgl-interactive:active,
29 | .mapboxgl-ctrl-group > button.mapboxgl-ctrl-compass:active {
30 | cursor: -webkit-grabbing;
31 | cursor: -moz-grabbing;
32 | cursor: grabbing;
33 | }
34 |
35 | .mapboxgl-canvas-container.mapboxgl-touch-zoom-rotate,
36 | .mapboxgl-canvas-container.mapboxgl-touch-zoom-rotate .mapboxgl-canvas {
37 | touch-action: pan-x pan-y;
38 | }
39 |
40 | .mapboxgl-canvas-container.mapboxgl-touch-drag-pan,
41 | .mapboxgl-canvas-container.mapboxgl-touch-drag-pan .mapboxgl-canvas {
42 | touch-action: pinch-zoom;
43 | }
44 |
45 | .mapboxgl-canvas-container.mapboxgl-touch-zoom-rotate.mapboxgl-touch-drag-pan,
46 | .mapboxgl-canvas-container.mapboxgl-touch-zoom-rotate.mapboxgl-touch-drag-pan .mapboxgl-canvas {
47 | touch-action: none;
48 | }
49 |
50 | .mapboxgl-ctrl-top-left,
51 | .mapboxgl-ctrl-top-right,
52 | .mapboxgl-ctrl-bottom-left,
53 | .mapboxgl-ctrl-bottom-right { position: absolute; pointer-events: none; z-index: 2; }
54 | .mapboxgl-ctrl-top-left { top: 0; left: 0; }
55 | .mapboxgl-ctrl-top-right { top: 0; right: 0; }
56 | .mapboxgl-ctrl-bottom-left { bottom: 0; left: 0; }
57 | .mapboxgl-ctrl-bottom-right { right: 0; bottom: 0; }
58 |
59 | .mapboxgl-ctrl { clear: both; pointer-events: auto; }
60 | .mapboxgl-ctrl-top-left .mapboxgl-ctrl { margin: 10px 0 0 10px; float: left; }
61 | .mapboxgl-ctrl-top-right .mapboxgl-ctrl { margin: 10px 10px 0 0; float: right; }
62 | .mapboxgl-ctrl-bottom-left .mapboxgl-ctrl { margin: 0 0 10px 10px; float: left; }
63 | .mapboxgl-ctrl-bottom-right .mapboxgl-ctrl { margin: 0 10px 10px 0; float: right; }
64 |
65 | .mapboxgl-ctrl-group {
66 | border-radius: 4px;
67 | overflow: hidden;
68 | background: #fff;
69 | }
70 |
71 | .mapboxgl-ctrl-group:not(:empty) {
72 | -moz-box-shadow: 0 0 2px rgba(0, 0, 0, 0.1);
73 | -webkit-box-shadow: 0 0 2px rgba(0, 0, 0, 0.1);
74 | box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
75 | }
76 |
77 | .mapboxgl-ctrl-group > button {
78 | width: 30px;
79 | height: 30px;
80 | display: block;
81 | padding: 0;
82 | outline: none;
83 | border: 0;
84 | box-sizing: border-box;
85 | background-color: transparent;
86 | cursor: pointer;
87 | }
88 |
89 | .mapboxgl-ctrl-group > button + button {
90 | border-top: 1px solid #ddd;
91 | }
92 |
93 | /* https://bugzilla.mozilla.org/show_bug.cgi?id=140562 */
94 | .mapboxgl-ctrl > button::-moz-focus-inner {
95 | border: 0;
96 | padding: 0;
97 | }
98 |
99 | .mapboxgl-ctrl > button:hover {
100 | background-color: rgba(0, 0, 0, 0.05);
101 | }
102 |
103 | .mapboxgl-ctrl-icon,
104 | .mapboxgl-ctrl-icon > .mapboxgl-ctrl-compass-arrow {
105 | speak: none;
106 | -webkit-font-smoothing: antialiased;
107 | -moz-osx-font-smoothing: grayscale;
108 | }
109 |
110 | .mapboxgl-ctrl-icon {
111 | padding: 5px;
112 | }
113 |
114 | .mapboxgl-ctrl-icon.mapboxgl-ctrl-zoom-out {
115 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3E %3Cpath style='fill:%23333333;' d='m 7,9 c -0.554,0 -1,0.446 -1,1 0,0.554 0.446,1 1,1 l 6,0 c 0.554,0 1,-0.446 1,-1 0,-0.554 -0.446,-1 -1,-1 z'/%3E %3C/svg%3E");
116 | }
117 |
118 | .mapboxgl-ctrl-icon.mapboxgl-ctrl-zoom-in {
119 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3E %3Cpath style='fill:%23333333;' d='M 10 6 C 9.446 6 9 6.4459904 9 7 L 9 9 L 7 9 C 6.446 9 6 9.446 6 10 C 6 10.554 6.446 11 7 11 L 9 11 L 9 13 C 9 13.55401 9.446 14 10 14 C 10.554 14 11 13.55401 11 13 L 11 11 L 13 11 C 13.554 11 14 10.554 14 10 C 14 9.446 13.554 9 13 9 L 11 9 L 11 7 C 11 6.4459904 10.554 6 10 6 z'/%3E %3C/svg%3E");
120 | }
121 |
122 | .mapboxgl-ctrl-icon.mapboxgl-ctrl-geolocate {
123 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23333'%3E %3Cpath d='M10 4C9 4 9 5 9 5L9 5.1A5 5 0 0 0 5.1 9L5 9C5 9 4 9 4 10 4 11 5 11 5 11L5.1 11A5 5 0 0 0 9 14.9L9 15C9 15 9 16 10 16 11 16 11 15 11 15L11 14.9A5 5 0 0 0 14.9 11L15 11C15 11 16 11 16 10 16 9 15 9 15 9L14.9 9A5 5 0 0 0 11 5.1L11 5C11 5 11 4 10 4zM10 6.5A3.5 3.5 0 0 1 13.5 10 3.5 3.5 0 0 1 10 13.5 3.5 3.5 0 0 1 6.5 10 3.5 3.5 0 0 1 10 6.5zM10 8.3A1.8 1.8 0 0 0 8.3 10 1.8 1.8 0 0 0 10 11.8 1.8 1.8 0 0 0 11.8 10 1.8 1.8 0 0 0 10 8.3z'/%3E %3C/svg%3E");
124 | }
125 |
126 | .mapboxgl-ctrl-icon.mapboxgl-ctrl-geolocate:disabled {
127 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23aaa'%3E %3Cpath d='M10 4C9 4 9 5 9 5L9 5.1A5 5 0 0 0 5.1 9L5 9C5 9 4 9 4 10 4 11 5 11 5 11L5.1 11A5 5 0 0 0 9 14.9L9 15C9 15 9 16 10 16 11 16 11 15 11 15L11 14.9A5 5 0 0 0 14.9 11L15 11C15 11 16 11 16 10 16 9 15 9 15 9L14.9 9A5 5 0 0 0 11 5.1L11 5C11 5 11 4 10 4zM10 6.5A3.5 3.5 0 0 1 13.5 10 3.5 3.5 0 0 1 10 13.5 3.5 3.5 0 0 1 6.5 10 3.5 3.5 0 0 1 10 6.5zM10 8.3A1.8 1.8 0 0 0 8.3 10 1.8 1.8 0 0 0 10 11.8 1.8 1.8 0 0 0 11.8 10 1.8 1.8 0 0 0 10 8.3z'/%3E %3C/svg%3E");
128 | }
129 |
130 | .mapboxgl-ctrl-icon.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-active {
131 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%2333b5e5'%3E %3Cpath d='M10 4C9 4 9 5 9 5L9 5.1A5 5 0 0 0 5.1 9L5 9C5 9 4 9 4 10 4 11 5 11 5 11L5.1 11A5 5 0 0 0 9 14.9L9 15C9 15 9 16 10 16 11 16 11 15 11 15L11 14.9A5 5 0 0 0 14.9 11L15 11C15 11 16 11 16 10 16 9 15 9 15 9L14.9 9A5 5 0 0 0 11 5.1L11 5C11 5 11 4 10 4zM10 6.5A3.5 3.5 0 0 1 13.5 10 3.5 3.5 0 0 1 10 13.5 3.5 3.5 0 0 1 6.5 10 3.5 3.5 0 0 1 10 6.5zM10 8.3A1.8 1.8 0 0 0 8.3 10 1.8 1.8 0 0 0 10 11.8 1.8 1.8 0 0 0 11.8 10 1.8 1.8 0 0 0 10 8.3z'/%3E %3C/svg%3E");
132 | }
133 |
134 | .mapboxgl-ctrl-icon.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-active-error {
135 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23e58978'%3E %3Cpath d='M10 4C9 4 9 5 9 5L9 5.1A5 5 0 0 0 5.1 9L5 9C5 9 4 9 4 10 4 11 5 11 5 11L5.1 11A5 5 0 0 0 9 14.9L9 15C9 15 9 16 10 16 11 16 11 15 11 15L11 14.9A5 5 0 0 0 14.9 11L15 11C15 11 16 11 16 10 16 9 15 9 15 9L14.9 9A5 5 0 0 0 11 5.1L11 5C11 5 11 4 10 4zM10 6.5A3.5 3.5 0 0 1 13.5 10 3.5 3.5 0 0 1 10 13.5 3.5 3.5 0 0 1 6.5 10 3.5 3.5 0 0 1 10 6.5zM10 8.3A1.8 1.8 0 0 0 8.3 10 1.8 1.8 0 0 0 10 11.8 1.8 1.8 0 0 0 11.8 10 1.8 1.8 0 0 0 10 8.3z'/%3E %3C/svg%3E");
136 | }
137 |
138 | .mapboxgl-ctrl-icon.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-background {
139 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%2333b5e5'%3E %3Cpath d='M 10,4 C 9,4 9,5 9,5 L 9,5.1 C 7.0357113,5.5006048 5.5006048,7.0357113 5.1,9 L 5,9 c 0,0 -1,0 -1,1 0,1 1,1 1,1 l 0.1,0 c 0.4006048,1.964289 1.9357113,3.499395 3.9,3.9 L 9,15 c 0,0 0,1 1,1 1,0 1,-1 1,-1 l 0,-0.1 c 1.964289,-0.400605 3.499395,-1.935711 3.9,-3.9 l 0.1,0 c 0,0 1,0 1,-1 C 16,9 15,9 15,9 L 14.9,9 C 14.499395,7.0357113 12.964289,5.5006048 11,5.1 L 11,5 c 0,0 0,-1 -1,-1 z m 0,2.5 c 1.932997,0 3.5,1.5670034 3.5,3.5 0,1.932997 -1.567003,3.5 -3.5,3.5 C 8.0670034,13.5 6.5,11.932997 6.5,10 6.5,8.0670034 8.0670034,6.5 10,6.5 Z'/%3E %3C/svg%3E");
140 | }
141 |
142 | .mapboxgl-ctrl-icon.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-background-error {
143 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23e54e33'%3E %3Cpath d='M 10,4 C 9,4 9,5 9,5 L 9,5.1 C 7.0357113,5.5006048 5.5006048,7.0357113 5.1,9 L 5,9 c 0,0 -1,0 -1,1 0,1 1,1 1,1 l 0.1,0 c 0.4006048,1.964289 1.9357113,3.499395 3.9,3.9 L 9,15 c 0,0 0,1 1,1 1,0 1,-1 1,-1 l 0,-0.1 c 1.964289,-0.400605 3.499395,-1.935711 3.9,-3.9 l 0.1,0 c 0,0 1,0 1,-1 C 16,9 15,9 15,9 L 14.9,9 C 14.499395,7.0357113 12.964289,5.5006048 11,5.1 L 11,5 c 0,0 0,-1 -1,-1 z m 0,2.5 c 1.932997,0 3.5,1.5670034 3.5,3.5 0,1.932997 -1.567003,3.5 -3.5,3.5 C 8.0670034,13.5 6.5,11.932997 6.5,10 6.5,8.0670034 8.0670034,6.5 10,6.5 Z'/%3E %3C/svg%3E");
144 | }
145 |
146 | .mapboxgl-ctrl-icon.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-waiting {
147 | -webkit-animation: mapboxgl-spin 2s infinite linear;
148 | -moz-animation: mapboxgl-spin 2s infinite linear;
149 | -o-animation: mapboxgl-spin 2s infinite linear;
150 | -ms-animation: mapboxgl-spin 2s infinite linear;
151 | animation: mapboxgl-spin 2s infinite linear;
152 | }
153 |
154 | @-webkit-keyframes mapboxgl-spin {
155 | 0% { -webkit-transform: rotate(0deg); }
156 | 100% { -webkit-transform: rotate(360deg); }
157 | }
158 |
159 | @-moz-keyframes mapboxgl-spin {
160 | 0% { -moz-transform: rotate(0deg); }
161 | 100% { -moz-transform: rotate(360deg); }
162 | }
163 |
164 | @-o-keyframes mapboxgl-spin {
165 | 0% { -o-transform: rotate(0deg); }
166 | 100% { -o-transform: rotate(360deg); }
167 | }
168 |
169 | @-ms-keyframes mapboxgl-spin {
170 | 0% { -ms-transform: rotate(0deg); }
171 | 100% { -ms-transform: rotate(360deg); }
172 | }
173 |
174 | @keyframes mapboxgl-spin {
175 | 0% { transform: rotate(0deg); }
176 | 100% { transform: rotate(360deg); }
177 | }
178 |
179 | .mapboxgl-ctrl-icon.mapboxgl-ctrl-fullscreen {
180 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3E %3Cpath d='M 5 4 C 4.5 4 4 4.5 4 5 L 4 6 L 4 9 L 4.5 9 L 5.7773438 7.296875 C 6.7771319 8.0602131 7.835765 8.9565728 8.890625 10 C 7.8257121 11.0633 6.7761791 11.951675 5.78125 12.707031 L 4.5 11 L 4 11 L 4 15 C 4 15.5 4.5 16 5 16 L 9 16 L 9 15.5 L 7.2734375 14.205078 C 8.0428931 13.187886 8.9395441 12.133481 9.9609375 11.068359 C 11.042371 12.14699 11.942093 13.2112 12.707031 14.21875 L 11 15.5 L 11 16 L 14 16 L 15 16 C 15.5 16 16 15.5 16 15 L 16 14 L 16 11 L 15.5 11 L 14.205078 12.726562 C 13.177985 11.949617 12.112718 11.043577 11.037109 10.009766 C 12.151856 8.981061 13.224345 8.0798624 14.228516 7.3046875 L 15.5 9 L 16 9 L 16 5 C 16 4.5 15.5 4 15 4 L 11 4 L 11 4.5 L 12.703125 5.7773438 C 11.932647 6.7864834 11.026693 7.8554712 9.9707031 8.9199219 C 8.9584739 7.8204943 8.0698767 6.7627188 7.3046875 5.7714844 L 9 4.5 L 9 4 L 6 4 L 5 4 z '/%3E %3C/svg%3E");
181 | }
182 |
183 | .mapboxgl-ctrl-icon.mapboxgl-ctrl-shrink {
184 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3E %3Cpath style='fill:%23000000;' d='M 4.2421875 3.4921875 A 0.750075 0.750075 0 0 0 3.71875 4.78125 L 5.9648438 7.0273438 L 4 8.5 L 4 9 L 8 9 C 8.500001 8.9999988 9 8.4999992 9 8 L 9 4 L 8.5 4 L 7.0175781 5.9550781 L 4.78125 3.71875 A 0.750075 0.750075 0 0 0 4.2421875 3.4921875 z M 15.734375 3.4921875 A 0.750075 0.750075 0 0 0 15.21875 3.71875 L 12.984375 5.953125 L 11.5 4 L 11 4 L 11 8 C 11 8.4999992 11.499999 8.9999988 12 9 L 16 9 L 16 8.5 L 14.035156 7.0273438 L 16.28125 4.78125 A 0.750075 0.750075 0 0 0 15.734375 3.4921875 z M 4 11 L 4 11.5 L 5.9648438 12.972656 L 3.71875 15.21875 A 0.75130096 0.75130096 0 1 0 4.78125 16.28125 L 7.0273438 14.035156 L 8.5 16 L 9 16 L 9 12 C 9 11.500001 8.500001 11.000001 8 11 L 4 11 z M 12 11 C 11.499999 11.000001 11 11.500001 11 12 L 11 16 L 11.5 16 L 12.972656 14.035156 L 15.21875 16.28125 A 0.75130096 0.75130096 0 1 0 16.28125 15.21875 L 14.035156 12.972656 L 16 11.5 L 16 11 L 12 11 z '/%3E %3C/svg%3E");
185 | }
186 |
187 | .mapboxgl-ctrl-icon.mapboxgl-ctrl-compass > .mapboxgl-ctrl-compass-arrow {
188 | width: 20px;
189 | height: 20px;
190 | margin: 5px;
191 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3E %3Cpolygon fill='%23333333' points='6,9 10,1 14,9'/%3E %3Cpolygon fill='%23CCCCCC' points='6,11 10,19 14,11 '/%3E %3C/svg%3E");
192 | background-repeat: no-repeat;
193 | display: inline-block;
194 | }
195 |
196 | a.mapboxgl-ctrl-logo {
197 | width: 85px;
198 | height: 21px;
199 | margin: 0 0 -3px -3px;
200 | display: block;
201 | background-repeat: no-repeat;
202 | cursor: pointer;
203 | background-image: url("data:image/svg+xml;charset=utf-8,%3C?xml version='1.0' encoding='utf-8'?%3E%3Csvg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 84.49 21' style='enable-background:new 0 0 84.49 21;' xml:space='preserve'%3E%3Cg%3E %3Cpath class='st0' style='opacity:0.9; fill: %23FFFFFF; enable-background: new;' d='M83.25,14.26c0,0.12-0.09,0.21-0.21,0.21h-1.61c-0.13,0-0.24-0.06-0.3-0.17l-1.44-2.39l-1.44,2.39 c-0.06,0.11-0.18,0.17-0.3,0.17h-1.61c-0.04,0-0.08-0.01-0.12-0.03c-0.09-0.06-0.13-0.19-0.06-0.28l0,0l2.43-3.68L76.2,6.84 c-0.02-0.03-0.03-0.07-0.03-0.12c0-0.12,0.09-0.21,0.21-0.21h1.61c0.13,0,0.24,0.06,0.3,0.17l1.41,2.36l1.4-2.35 c0.06-0.11,0.18-0.17,0.3-0.17H83c0.04,0,0.08,0.01,0.12,0.03c0.09,0.06,0.13,0.19,0.06,0.28l0,0l-2.37,3.63l2.43,3.67 C83.24,14.18,83.25,14.22,83.25,14.26z'/%3E %3Cpath class='st0' style='opacity:0.9; fill: %23FFFFFF; enable-background: new;' d='M66.24,9.59c-0.39-1.88-1.96-3.28-3.84-3.28c-1.03,0-2.03,0.42-2.73,1.18V3.51c0-0.13-0.1-0.23-0.23-0.23h-1.4 c-0.13,0-0.23,0.11-0.23,0.23v10.72c0,0.13,0.1,0.23,0.23,0.23h1.4c0.13,0,0.23-0.11,0.23-0.23V13.5c0.71,0.75,1.7,1.18,2.73,1.18 c1.88,0,3.45-1.41,3.84-3.29C66.37,10.79,66.37,10.18,66.24,9.59L66.24,9.59z M62.08,13c-1.32,0-2.39-1.11-2.41-2.48v-0.06 c0.02-1.38,1.09-2.48,2.41-2.48s2.42,1.12,2.42,2.51S63.41,13,62.08,13z'/%3E %3Cpath class='st0' style='opacity:0.9; fill: %23FFFFFF; enable-background: new;' d='M71.67,6.32c-1.98-0.01-3.72,1.35-4.16,3.29c-0.13,0.59-0.13,1.19,0,1.77c0.44,1.94,2.17,3.32,4.17,3.3 c2.35,0,4.26-1.87,4.26-4.19S74.04,6.32,71.67,6.32z M71.65,13.01c-1.33,0-2.42-1.12-2.42-2.51s1.08-2.52,2.42-2.52 c1.33,0,2.42,1.12,2.42,2.51S72.99,13,71.65,13.01L71.65,13.01z'/%3E %3Cpath class='st1' style='opacity:0.35; enable-background:new;' d='M62.08,7.98c-1.32,0-2.39,1.11-2.41,2.48v0.06C59.68,11.9,60.75,13,62.08,13s2.42-1.12,2.42-2.51 S63.41,7.98,62.08,7.98z M62.08,11.76c-0.63,0-1.14-0.56-1.17-1.25v-0.04c0.01-0.69,0.54-1.25,1.17-1.25 c0.63,0,1.17,0.57,1.17,1.27C63.24,11.2,62.73,11.76,62.08,11.76z'/%3E %3Cpath class='st1' style='opacity:0.35; enable-background:new;' d='M71.65,7.98c-1.33,0-2.42,1.12-2.42,2.51S70.32,13,71.65,13s2.42-1.12,2.42-2.51S72.99,7.98,71.65,7.98z M71.65,11.76c-0.64,0-1.17-0.57-1.17-1.27c0-0.7,0.53-1.26,1.17-1.26s1.17,0.57,1.17,1.27C72.82,11.21,72.29,11.76,71.65,11.76z'/%3E %3Cpath class='st0' style='opacity:0.9; fill: %23FFFFFF; enable-background: new;' d='M45.74,6.53h-1.4c-0.13,0-0.23,0.11-0.23,0.23v0.73c-0.71-0.75-1.7-1.18-2.73-1.18 c-2.17,0-3.94,1.87-3.94,4.19s1.77,4.19,3.94,4.19c1.04,0,2.03-0.43,2.73-1.19v0.73c0,0.13,0.1,0.23,0.23,0.23h1.4 c0.13,0,0.23-0.11,0.23-0.23V6.74c0-0.12-0.09-0.22-0.22-0.22C45.75,6.53,45.75,6.53,45.74,6.53z M44.12,10.53 C44.11,11.9,43.03,13,41.71,13s-2.42-1.12-2.42-2.51s1.08-2.52,2.4-2.52c1.33,0,2.39,1.11,2.41,2.48L44.12,10.53z'/%3E %3Cpath class='st1' style='opacity:0.35; enable-background:new;' d='M41.71,7.98c-1.33,0-2.42,1.12-2.42,2.51S40.37,13,41.71,13s2.39-1.11,2.41-2.48v-0.06 C44.1,9.09,43.03,7.98,41.71,7.98z M40.55,10.49c0-0.7,0.52-1.27,1.17-1.27c0.64,0,1.14,0.56,1.17,1.25v0.04 c-0.01,0.68-0.53,1.24-1.17,1.24C41.08,11.75,40.55,11.19,40.55,10.49z'/%3E %3Cpath class='st0' style='opacity:0.9; fill: %23FFFFFF; enable-background: new;' d='M52.41,6.32c-1.03,0-2.03,0.42-2.73,1.18V6.75c0-0.13-0.1-0.23-0.23-0.23h-1.4c-0.13,0-0.23,0.11-0.23,0.23 v10.72c0,0.13,0.1,0.23,0.23,0.23h1.4c0.13,0,0.23-0.1,0.23-0.23V13.5c0.71,0.75,1.7,1.18,2.74,1.18c2.17,0,3.94-1.87,3.94-4.19 S54.58,6.32,52.41,6.32z M52.08,13.01c-1.32,0-2.39-1.11-2.42-2.48v-0.07c0.02-1.38,1.09-2.49,2.4-2.49c1.32,0,2.41,1.12,2.41,2.51 S53.4,13,52.08,13.01L52.08,13.01z'/%3E %3Cpath class='st1' style='opacity:0.35; enable-background:new;' d='M52.08,7.98c-1.32,0-2.39,1.11-2.42,2.48v0.06c0.03,1.38,1.1,2.48,2.42,2.48s2.41-1.12,2.41-2.51 S53.4,7.98,52.08,7.98z M52.08,11.76c-0.63,0-1.14-0.56-1.17-1.25v-0.04c0.01-0.69,0.54-1.25,1.17-1.25c0.63,0,1.17,0.58,1.17,1.27 S52.72,11.76,52.08,11.76z'/%3E %3Cpath class='st0' style='opacity:0.9; fill: %23FFFFFF; enable-background: new;' d='M36.08,14.24c0,0.13-0.1,0.23-0.23,0.23h-1.41c-0.13,0-0.23-0.11-0.23-0.23V9.68c0-0.98-0.74-1.71-1.62-1.71 c-0.8,0-1.46,0.7-1.59,1.62l0.01,4.66c0,0.13-0.11,0.23-0.23,0.23h-1.41c-0.13,0-0.23-0.11-0.23-0.23V9.68 c0-0.98-0.74-1.71-1.62-1.71c-0.85,0-1.54,0.79-1.6,1.8v4.48c0,0.13-0.1,0.23-0.23,0.23h-1.4c-0.13,0-0.23-0.11-0.23-0.23V6.74 c0.01-0.13,0.1-0.22,0.23-0.22h1.4c0.13,0,0.22,0.11,0.23,0.22V7.4c0.5-0.68,1.3-1.09,2.16-1.1h0.03c1.09,0,2.09,0.6,2.6,1.55 c0.45-0.95,1.4-1.55,2.44-1.56c1.62,0,2.93,1.25,2.9,2.78L36.08,14.24z'/%3E %3Cpath class='st1' style='opacity:0.35; enable-background:new;' d='M84.34,13.59l-0.07-0.13l-1.96-2.99l1.94-2.95c0.44-0.67,0.26-1.56-0.41-2.02c-0.02,0-0.03,0-0.04-0.01 c-0.23-0.15-0.5-0.22-0.78-0.22h-1.61c-0.56,0-1.08,0.29-1.37,0.78L79.72,6.6l-0.34-0.56C79.09,5.56,78.57,5.27,78,5.27h-1.6 c-0.6,0-1.13,0.37-1.35,0.92c-2.19-1.66-5.28-1.47-7.26,0.45c-0.35,0.34-0.65,0.72-0.89,1.14c-0.9-1.62-2.58-2.72-4.5-2.72 c-0.5,0-1.01,0.07-1.48,0.23V3.51c0-0.82-0.66-1.48-1.47-1.48h-1.4c-0.81,0-1.47,0.66-1.47,1.47v3.75 c-0.95-1.36-2.5-2.18-4.17-2.19c-0.74,0-1.46,0.16-2.12,0.47c-0.24-0.17-0.54-0.26-0.84-0.26h-1.4c-0.45,0-0.87,0.21-1.15,0.56 c-0.02-0.03-0.04-0.05-0.07-0.08c-0.28-0.3-0.68-0.47-1.09-0.47h-1.39c-0.3,0-0.6,0.09-0.84,0.26c-0.67-0.3-1.39-0.46-2.12-0.46 c-1.83,0-3.43,1-4.37,2.5c-0.2-0.46-0.48-0.89-0.83-1.25c-0.8-0.81-1.89-1.25-3.02-1.25h-0.01c-0.89,0.01-1.75,0.33-2.46,0.88 c-0.74-0.57-1.64-0.88-2.57-0.88H28.1c-0.29,0-0.58,0.03-0.86,0.11c-0.28,0.06-0.56,0.16-0.82,0.28c-0.21-0.12-0.45-0.18-0.7-0.18 h-1.4c-0.82,0-1.47,0.66-1.47,1.47v7.5c0,0.82,0.66,1.47,1.47,1.47h1.4c0.82,0,1.48-0.66,1.48-1.48l0,0V9.79 c0.03-0.36,0.23-0.59,0.36-0.59c0.18,0,0.38,0.18,0.38,0.47v4.57c0,0.82,0.66,1.47,1.47,1.47h1.41c0.82,0,1.47-0.66,1.47-1.47 l-0.01-4.57c0.06-0.32,0.25-0.47,0.35-0.47c0.18,0,0.38,0.18,0.38,0.47v4.57c0,0.82,0.66,1.47,1.47,1.47h1.41 c0.82,0,1.47-0.66,1.47-1.47v-0.38c0.96,1.29,2.46,2.06,4.06,2.06c0.74,0,1.46-0.16,2.12-0.47c0.24,0.17,0.54,0.26,0.84,0.26h1.39 c0.3,0,0.6-0.09,0.84-0.26v2.01c0,0.82,0.66,1.47,1.47,1.47h1.4c0.82,0,1.47-0.66,1.47-1.47v-1.77c0.48,0.15,0.99,0.23,1.49,0.22 c1.7,0,3.22-0.87,4.17-2.2v0.52c0,0.82,0.66,1.47,1.47,1.47h1.4c0.3,0,0.6-0.09,0.84-0.26c0.66,0.31,1.39,0.47,2.12,0.47 c1.92,0,3.6-1.1,4.49-2.73c1.54,2.65,4.95,3.53,7.58,1.98c0.18-0.11,0.36-0.22,0.53-0.36c0.22,0.55,0.76,0.91,1.35,0.9H78 c0.56,0,1.08-0.29,1.37-0.78l0.37-0.61l0.37,0.61c0.29,0.48,0.81,0.78,1.38,0.78h1.6c0.81,0,1.46-0.66,1.45-1.46 C84.49,14.02,84.44,13.8,84.34,13.59L84.34,13.59z M35.86,14.47h-1.41c-0.13,0-0.23-0.11-0.23-0.23V9.68 c0-0.98-0.74-1.71-1.62-1.71c-0.8,0-1.46,0.7-1.59,1.62l0.01,4.66c0,0.13-0.1,0.23-0.23,0.23h-1.41c-0.13,0-0.23-0.11-0.23-0.23 V9.68c0-0.98-0.74-1.71-1.62-1.71c-0.85,0-1.54,0.79-1.6,1.8v4.48c0,0.13-0.1,0.23-0.23,0.23h-1.4c-0.13,0-0.23-0.11-0.23-0.23 V6.74c0.01-0.13,0.11-0.22,0.23-0.22h1.4c0.13,0,0.22,0.11,0.23,0.22V7.4c0.5-0.68,1.3-1.09,2.16-1.1h0.03 c1.09,0,2.09,0.6,2.6,1.55c0.45-0.95,1.4-1.55,2.44-1.56c1.62,0,2.93,1.25,2.9,2.78l0.01,5.16C36.09,14.36,35.98,14.46,35.86,14.47 L35.86,14.47z M45.97,14.24c0,0.13-0.1,0.23-0.23,0.23h-1.4c-0.13,0-0.23-0.11-0.23-0.23V13.5c-0.7,0.76-1.69,1.18-2.72,1.18 c-2.17,0-3.94-1.87-3.94-4.19s1.77-4.19,3.94-4.19c1.03,0,2.02,0.43,2.73,1.18V6.74c0-0.13,0.1-0.23,0.23-0.23h1.4 c0.12-0.01,0.22,0.08,0.23,0.21c0,0.01,0,0.01,0,0.02v7.51h-0.01V14.24z M52.41,14.67c-1.03,0-2.02-0.43-2.73-1.18v3.97 c0,0.13-0.1,0.23-0.23,0.23h-1.4c-0.13,0-0.23-0.1-0.23-0.23V6.75c0-0.13,0.1-0.22,0.23-0.22h1.4c0.13,0,0.23,0.11,0.23,0.23v0.73 c0.71-0.76,1.7-1.18,2.73-1.18c2.17,0,3.94,1.86,3.94,4.18S54.58,14.67,52.41,14.67z M66.24,11.39c-0.39,1.87-1.96,3.29-3.84,3.29 c-1.03,0-2.02-0.43-2.73-1.18v0.73c0,0.13-0.1,0.23-0.23,0.23h-1.4c-0.13,0-0.23-0.11-0.23-0.23V3.51c0-0.13,0.1-0.23,0.23-0.23 h1.4c0.13,0,0.23,0.11,0.23,0.23v3.97c0.71-0.75,1.7-1.18,2.73-1.17c1.88,0,3.45,1.4,3.84,3.28C66.37,10.19,66.37,10.8,66.24,11.39 L66.24,11.39L66.24,11.39z M71.67,14.68c-2,0.01-3.73-1.35-4.17-3.3c-0.13-0.59-0.13-1.19,0-1.77c0.44-1.94,2.17-3.31,4.17-3.3 c2.36,0,4.26,1.87,4.26,4.19S74.03,14.68,71.67,14.68L71.67,14.68z M83.04,14.47h-1.61c-0.13,0-0.24-0.06-0.3-0.17l-1.44-2.39 l-1.44,2.39c-0.06,0.11-0.18,0.17-0.3,0.17h-1.61c-0.04,0-0.08-0.01-0.12-0.03c-0.09-0.06-0.13-0.19-0.06-0.28l0,0l2.43-3.68 L76.2,6.84c-0.02-0.03-0.03-0.07-0.03-0.12c0-0.12,0.09-0.21,0.21-0.21h1.61c0.13,0,0.24,0.06,0.3,0.17l1.41,2.36l1.41-2.36 c0.06-0.11,0.18-0.17,0.3-0.17h1.61c0.04,0,0.08,0.01,0.12,0.03c0.09,0.06,0.13,0.19,0.06,0.28l0,0l-2.38,3.64l2.43,3.67 c0.02,0.03,0.03,0.07,0.03,0.12C83.25,14.38,83.16,14.47,83.04,14.47L83.04,14.47L83.04,14.47z'/%3E %3Cpath class='st0' style='opacity:0.9; fill: %23FFFFFF; enable-background: new;' d='M10.5,1.24c-5.11,0-9.25,4.15-9.25,9.25s4.15,9.25,9.25,9.25s9.25-4.15,9.25-9.25 C19.75,5.38,15.61,1.24,10.5,1.24z M14.89,12.77c-1.93,1.93-4.78,2.31-6.7,2.31c-0.7,0-1.41-0.05-2.1-0.16c0,0-1.02-5.64,2.14-8.81 c0.83-0.83,1.95-1.28,3.13-1.28c1.27,0,2.49,0.51,3.39,1.42C16.59,8.09,16.64,11,14.89,12.77z'/%3E %3Cpath class='st1' style='opacity:0.35; enable-background:new;' d='M10.5-0.01C4.7-0.01,0,4.7,0,10.49s4.7,10.5,10.5,10.5S21,16.29,21,10.49C20.99,4.7,16.3-0.01,10.5-0.01z M10.5,19.74c-5.11,0-9.25-4.15-9.25-9.25s4.14-9.26,9.25-9.26s9.25,4.15,9.25,9.25C19.75,15.61,15.61,19.74,10.5,19.74z'/%3E %3Cpath class='st1' style='opacity:0.35; enable-background:new;' d='M14.74,6.25C12.9,4.41,9.98,4.35,8.23,6.1c-3.16,3.17-2.14,8.81-2.14,8.81s5.64,1.02,8.81-2.14 C16.64,11,16.59,8.09,14.74,6.25z M12.47,10.34l-0.91,1.87l-0.9-1.87L8.8,9.43l1.86-0.9l0.9-1.87l0.91,1.87l1.86,0.9L12.47,10.34z'/%3E %3Cpolygon class='st0' style='opacity:0.9; fill: %23FFFFFF; enable-background: new;' points='14.33,9.43 12.47,10.34 11.56,12.21 10.66,10.34 8.8,9.43 10.66,8.53 11.56,6.66 12.47,8.53 '/%3E%3C/g%3E%3C/svg%3E");
204 | }
205 |
206 | a.mapboxgl-ctrl-logo.mapboxgl-compact {
207 | width: 21px;
208 | height: 21px;
209 | background-image: url("data:image/svg+xml;charset=utf-8,%3C?xml version='1.0' encoding='utf-8'?%3E %3Csvg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 21 21' style='enable-background:new 0 0 21 21;' xml:space='preserve'%3E%3Cg transform='translate(0,0.01)'%3E%3Cpath d='m 10.5,1.24 c -5.11,0 -9.25,4.15 -9.25,9.25 0,5.1 4.15,9.25 9.25,9.25 5.1,0 9.25,-4.15 9.25,-9.25 0,-5.11 -4.14,-9.25 -9.25,-9.25 z m 4.39,11.53 c -1.93,1.93 -4.78,2.31 -6.7,2.31 -0.7,0 -1.41,-0.05 -2.1,-0.16 0,0 -1.02,-5.64 2.14,-8.81 0.83,-0.83 1.95,-1.28 3.13,-1.28 1.27,0 2.49,0.51 3.39,1.42 1.84,1.84 1.89,4.75 0.14,6.52 z' style='opacity:0.9;fill:%23ffffff;enable-background:new' class='st0'/%3E%3Cpath d='M 10.5,-0.01 C 4.7,-0.01 0,4.7 0,10.49 c 0,5.79 4.7,10.5 10.5,10.5 5.8,0 10.5,-4.7 10.5,-10.5 C 20.99,4.7 16.3,-0.01 10.5,-0.01 Z m 0,19.75 c -5.11,0 -9.25,-4.15 -9.25,-9.25 0,-5.1 4.14,-9.26 9.25,-9.26 5.11,0 9.25,4.15 9.25,9.25 0,5.13 -4.14,9.26 -9.25,9.26 z' style='opacity:0.35;enable-background:new' class='st1'/%3E%3Cpath d='M 14.74,6.25 C 12.9,4.41 9.98,4.35 8.23,6.1 5.07,9.27 6.09,14.91 6.09,14.91 c 0,0 5.64,1.02 8.81,-2.14 C 16.64,11 16.59,8.09 14.74,6.25 Z m -2.27,4.09 -0.91,1.87 -0.9,-1.87 -1.86,-0.91 1.86,-0.9 0.9,-1.87 0.91,1.87 1.86,0.9 z' style='opacity:0.35;enable-background:new' class='st1'/%3E%3Cpolygon points='11.56,12.21 10.66,10.34 8.8,9.43 10.66,8.53 11.56,6.66 12.47,8.53 14.33,9.43 12.47,10.34 ' style='opacity:0.9;fill:%23ffffff;enable-background:new' class='st0'/%3E%3C/g%3E%3C/svg%3E");
210 | }
211 |
212 | .mapboxgl-ctrl.mapboxgl-ctrl-attrib {
213 | padding: 0 5px;
214 | background-color: rgba(255, 255, 255, 0.5);
215 | margin: 0;
216 | }
217 |
218 | @media screen {
219 | .mapboxgl-ctrl-attrib.mapboxgl-compact {
220 | margin: 0 10px 10px;
221 | position: relative;
222 | background-color: #fff;
223 | border-radius: 3px 12px 12px 3px;
224 | }
225 |
226 | .mapboxgl-ctrl-attrib.mapboxgl-compact:hover {
227 | padding: 2px 24px 2px 4px;
228 | visibility: visible;
229 | }
230 |
231 | .mapboxgl-ctrl-attrib.mapboxgl-compact > a {
232 | display: none;
233 | }
234 |
235 | .mapboxgl-ctrl-attrib.mapboxgl-compact:hover > a {
236 | display: inline;
237 | }
238 |
239 | .mapboxgl-ctrl-attrib.mapboxgl-compact::after {
240 | content: '';
241 | cursor: pointer;
242 | position: absolute;
243 | bottom: 0;
244 | right: 0;
245 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3E %3Cpath fill='%23333333' fill-rule='evenodd' d='M4,10a6,6 0 1,0 12,0a6,6 0 1,0 -12,0 M9,7a1,1 0 1,0 2,0a1,1 0 1,0 -2,0 M9,10a1,1 0 1,1 2,0l0,3a1,1 0 1,1 -2,0'/%3E %3C/svg%3E");
246 | background-color: rgba(255, 255, 255, 0.5);
247 | width: 24px;
248 | height: 24px;
249 | box-sizing: border-box;
250 | border-radius: 12px;
251 | }
252 | }
253 |
254 | .mapboxgl-ctrl-attrib a {
255 | color: rgba(0, 0, 0, 0.75);
256 | text-decoration: none;
257 | }
258 |
259 | .mapboxgl-ctrl-attrib a:hover {
260 | color: inherit;
261 | text-decoration: underline;
262 | }
263 |
264 | /* stylelint-disable-next-line selector-class-pattern */
265 | .mapboxgl-ctrl-attrib .mapbox-improve-map {
266 | font-weight: bold;
267 | margin-left: 2px;
268 | }
269 |
270 | .mapboxgl-attrib-empty {
271 | display: none;
272 | }
273 |
274 | .mapboxgl-ctrl-scale {
275 | background-color: rgba(255, 255, 255, 0.75);
276 | font-size: 10px;
277 | border-width: medium 2px 2px;
278 | border-style: none solid solid;
279 | border-color: #333;
280 | padding: 0 5px;
281 | color: #333;
282 | box-sizing: border-box;
283 | }
284 |
285 | .mapboxgl-popup {
286 | position: absolute;
287 | top: 0;
288 | left: 0;
289 | display: -webkit-flex;
290 | display: flex;
291 | will-change: transform;
292 | pointer-events: none;
293 | }
294 |
295 | .mapboxgl-popup-anchor-top,
296 | .mapboxgl-popup-anchor-top-left,
297 | .mapboxgl-popup-anchor-top-right {
298 | -webkit-flex-direction: column;
299 | flex-direction: column;
300 | }
301 |
302 | .mapboxgl-popup-anchor-bottom,
303 | .mapboxgl-popup-anchor-bottom-left,
304 | .mapboxgl-popup-anchor-bottom-right {
305 | -webkit-flex-direction: column-reverse;
306 | flex-direction: column-reverse;
307 | }
308 |
309 | .mapboxgl-popup-anchor-left {
310 | -webkit-flex-direction: row;
311 | flex-direction: row;
312 | }
313 |
314 | .mapboxgl-popup-anchor-right {
315 | -webkit-flex-direction: row-reverse;
316 | flex-direction: row-reverse;
317 | }
318 |
319 | .mapboxgl-popup-tip {
320 | width: 0;
321 | height: 0;
322 | border: 10px solid transparent;
323 | z-index: 1;
324 | }
325 |
326 | .mapboxgl-popup-anchor-top .mapboxgl-popup-tip {
327 | -webkit-align-self: center;
328 | align-self: center;
329 | border-top: none;
330 | border-bottom-color: #fff;
331 | }
332 |
333 | .mapboxgl-popup-anchor-top-left .mapboxgl-popup-tip {
334 | -webkit-align-self: flex-start;
335 | align-self: flex-start;
336 | border-top: none;
337 | border-left: none;
338 | border-bottom-color: #fff;
339 | }
340 |
341 | .mapboxgl-popup-anchor-top-right .mapboxgl-popup-tip {
342 | -webkit-align-self: flex-end;
343 | align-self: flex-end;
344 | border-top: none;
345 | border-right: none;
346 | border-bottom-color: #fff;
347 | }
348 |
349 | .mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip {
350 | -webkit-align-self: center;
351 | align-self: center;
352 | border-bottom: none;
353 | border-top-color: #fff;
354 | }
355 |
356 | .mapboxgl-popup-anchor-bottom-left .mapboxgl-popup-tip {
357 | -webkit-align-self: flex-start;
358 | align-self: flex-start;
359 | border-bottom: none;
360 | border-left: none;
361 | border-top-color: #fff;
362 | }
363 |
364 | .mapboxgl-popup-anchor-bottom-right .mapboxgl-popup-tip {
365 | -webkit-align-self: flex-end;
366 | align-self: flex-end;
367 | border-bottom: none;
368 | border-right: none;
369 | border-top-color: #fff;
370 | }
371 |
372 | .mapboxgl-popup-anchor-left .mapboxgl-popup-tip {
373 | -webkit-align-self: center;
374 | align-self: center;
375 | border-left: none;
376 | border-right-color: #fff;
377 | }
378 |
379 | .mapboxgl-popup-anchor-right .mapboxgl-popup-tip {
380 | -webkit-align-self: center;
381 | align-self: center;
382 | border-right: none;
383 | border-left-color: #fff;
384 | }
385 |
386 | .mapboxgl-popup-close-button {
387 | position: absolute;
388 | right: 0;
389 | top: 0;
390 | border: 0;
391 | border-radius: 0 3px 0 0;
392 | cursor: pointer;
393 | background-color: transparent;
394 | }
395 |
396 | .mapboxgl-popup-close-button:hover {
397 | background-color: rgba(0, 0, 0, 0.05);
398 | }
399 |
400 | .mapboxgl-popup-content {
401 | position: relative;
402 | background: #fff;
403 | border-radius: 3px;
404 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
405 | padding: 10px 10px 15px;
406 | pointer-events: auto;
407 | }
408 |
409 | .mapboxgl-popup-anchor-top-left .mapboxgl-popup-content {
410 | border-top-left-radius: 0;
411 | }
412 |
413 | .mapboxgl-popup-anchor-top-right .mapboxgl-popup-content {
414 | border-top-right-radius: 0;
415 | }
416 |
417 | .mapboxgl-popup-anchor-bottom-left .mapboxgl-popup-content {
418 | border-bottom-left-radius: 0;
419 | }
420 |
421 | .mapboxgl-popup-anchor-bottom-right .mapboxgl-popup-content {
422 | border-bottom-right-radius: 0;
423 | }
424 |
425 | .mapboxgl-marker {
426 | position: absolute;
427 | top: 0;
428 | left: 0;
429 | will-change: transform;
430 | }
431 |
432 | .mapboxgl-user-location-dot {
433 | background-color: #1da1f2;
434 | width: 15px;
435 | height: 15px;
436 | border-radius: 50%;
437 | box-shadow: 0 0 2px rgba(0, 0, 0, 0.25);
438 | }
439 |
440 | .mapboxgl-user-location-dot::before {
441 | background-color: #1da1f2;
442 | content: '';
443 | width: 15px;
444 | height: 15px;
445 | border-radius: 50%;
446 | position: absolute;
447 | -webkit-animation: mapboxgl-user-location-dot-pulse 2s infinite;
448 | -moz-animation: mapboxgl-user-location-dot-pulse 2s infinite;
449 | -ms-animation: mapboxgl-user-location-dot-pulse 2s infinite;
450 | animation: mapboxgl-user-location-dot-pulse 2s infinite;
451 | }
452 |
453 | .mapboxgl-user-location-dot::after {
454 | border-radius: 50%;
455 | border: 2px solid #fff;
456 | content: '';
457 | height: 19px;
458 | left: -2px;
459 | position: absolute;
460 | top: -2px;
461 | width: 19px;
462 | box-sizing: border-box;
463 | }
464 |
465 | @-webkit-keyframes mapboxgl-user-location-dot-pulse {
466 | 0% { -webkit-transform: scale(1); opacity: 1; }
467 | 70% { -webkit-transform: scale(3); opacity: 0; }
468 | 100% { -webkit-transform: scale(1); opacity: 0; }
469 | }
470 |
471 | @-ms-keyframes mapboxgl-user-location-dot-pulse {
472 | 0% { -ms-transform: scale(1); opacity: 1; }
473 | 70% { -ms-transform: scale(3); opacity: 0; }
474 | 100% { -ms-transform: scale(1); opacity: 0; }
475 | }
476 |
477 | @keyframes mapboxgl-user-location-dot-pulse {
478 | 0% { transform: scale(1); opacity: 1; }
479 | 70% { transform: scale(3); opacity: 0; }
480 | 100% { transform: scale(1); opacity: 0; }
481 | }
482 |
483 | .mapboxgl-user-location-dot-stale {
484 | background-color: #aaa;
485 | }
486 |
487 | .mapboxgl-user-location-dot-stale::after {
488 | display: none;
489 | }
490 |
491 | .mapboxgl-crosshair,
492 | .mapboxgl-crosshair .mapboxgl-interactive,
493 | .mapboxgl-crosshair .mapboxgl-interactive:active {
494 | cursor: crosshair;
495 | }
496 |
497 | .mapboxgl-boxzoom {
498 | position: absolute;
499 | top: 0;
500 | left: 0;
501 | width: 0;
502 | height: 0;
503 | background: #fff;
504 | border: 2px dotted #202020;
505 | opacity: 0.5;
506 | }
507 |
508 | @media print {
509 | /* stylelint-disable-next-line selector-class-pattern */
510 | .mapbox-improve-map {
511 | display: none;
512 | }
513 | }
514 |
--------------------------------------------------------------------------------