├── 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 |
73 |
0 changes:
74 |
75 | 76 | 77 |
78 | 79 |
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 | ![Screen layout](https://www.systemed.net/osm/conflation_screenshot.jpg "OSM Live Conflation") 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 | --------------------------------------------------------------------------------