├── .gitignore ├── LICENSE.txt ├── README.md ├── assets ├── pointer-large-focus.svg ├── pointer-large.svg ├── pointer-small-focus.svg └── pointer-small.svg ├── auth.js ├── build.js ├── index.css ├── index.html ├── index.js ├── land.html └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore these by default 2 | 3 | # node 4 | node_modules/ 5 | *.log 6 | 7 | # misc 8 | .DS_Store -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | ISC License 3 | 4 | Copyright (c) 2017, Mapbox 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## OpenStreetMap Navigation Map 2 | 3 | The OpenStreetMap Navigation map is a web application which uses [Mapillary vector layer](http://blog.mapillary.com/update/2015/05/27/vectortiles.html) to highlight the traffic signages detected in Mapillary images. The application is helpful to add/verify/modify turn-restrictions on OpenStreetMap. 4 | 5 | ![image](https://cloud.githubusercontent.com/assets/3423533/18305622/07e3b772-7506-11e6-9367-e1fed45eb10a.png) 6 | 7 | ### Develop 8 | 9 | * git clone 10 | * `npm install && npm start` 11 | 12 | ### Tilesets 13 | 14 | The map uses various custom Mapbox tilesets maintained by @planemad that has OSM navigation data extracted via overpass/[shaktimaan](https://github.com/geohacker/shaktiman). The tilesets are updated infrequently, please request an update if required. 15 | 16 | - Project cities boundaries - [`maning.worldcities`](https://www.mapbox.com/studio/tilesets/maning.worldcities/) 17 | - OSM turn restrictions `type=restriction` - [`planemad.osmturnrestrictions`](https://www.mapbox.com/studio/tilesets/planemad.osmturnrestrictions/) 18 | - OSM turn lanes `turn:lanes` `turn:lanes:forward` `turn:lanes:backward` - [`planemad.osmturnlanes`](https://www.mapbox.com/studio/tilesets/planemad.osmturnlanes/) 19 | - OSM traffic signals `highway=traffic_signals` - [`planemad.osmtrafficsignals`](https://www.mapbox.com/studio/tilesets/planemad.osmtrafficsignals/) 20 | - OSM speed limits `maxspeed=*` - [`planemad.osmmaxspeed`](https://www.mapbox.com/studio/tilesets/planemad.osmmaxspeed/) 21 | - OSM access restrictions `access=*` - [`planemad.osmaccess`](https://www.mapbox.com/studio/tilesets/planemad.osmaccess/) 22 | 23 | ### Interface 24 | 25 | OpenStreetMap Navigation Map consists of 3 layers which helps you to navigate between OpenStreetMap data and Mapillary data. 26 | 27 | ![image](https://cloud.githubusercontent.com/assets/3423533/18305101/91e3a9e4-7503-11e6-9cfc-857bccdcd322.png) 28 | 29 | - **Tile Boundaries** : Tile boundaries is a overlay layer which divides the map into tiles which is visible after zoom10. This layer helps to divides the working area/city into blocks and make sure that mappers don't work on the same area. 30 | 31 | ![image](https://cloud.githubusercontent.com/assets/3423533/18306061/c3270eb6-7507-11e6-9b16-8cc30b2273f5.png) 32 | 33 | *Image showing tile boundaries along with tile numbers* 34 | 35 | - **Oneways** : The Oneway layer highlights the roads with `oneway` tag in OpenStreetMap which helps the mapper to analyse the turn-restriction and the road data before adding it. 36 | 37 | ![image](https://cloud.githubusercontent.com/assets/3423533/18305778/b6806eec-7506-11e6-9831-7704f479b4a2.png) 38 | 39 | *Image showing `oneways` highlighted in map style* 40 | 41 | - **Mapillary Street Photos** : Mapillary Street Photos enables the Mapillary vector layer on the map which highlights the detected images with signages in it. The arrow on the GPS traces indicate the direction of the Mapillary sequence. 42 | 43 | ![mapillary-traces](https://cloud.githubusercontent.com/assets/3423533/18306030/a739ef34-7507-11e6-8385-b120b1774f92.png) 44 | 45 | *Image showing Mapillary traces in map style* 46 | 47 | ### Map Styles 48 | 49 | OpenStreetMap Navigation map highlights different features present in OpenStreetMap and Mapillary detected signs in different colours. Below is the description on colours used in the map style: 50 | 51 | - **Mapillary detected Turn-restriction** : The turn-restrictions detected on Mapillary vector layer are shown in the form on respective detected icons. The turn-restriction signages differ from country to country. Below are few examples: 52 | 53 | ![trs2](https://cloud.githubusercontent.com/assets/3423533/18307853/c9bbc9a8-750f-11e6-8784-a9236abf51b9.png) 54 | 55 | - **Turn-Restriction present on OpenStreetMap** : The map shows the turn-restrictions present on OpenStreetMap for the mapper to get the overview of the detection and go forward. 56 | - **No-turns** : No turn-restriction like `no-left-turn`, `no-right-turn`, `no-u-turn` and `no-straight-on` are styled with 57 | - `from` as yellow line. 58 | - `via` as red circle/dashed-line.(orange in case of `no-u-turn`) 59 | - `to` as red dashed-line. 60 | ![t1](https://cloud.githubusercontent.com/assets/3423533/18308365/d315c7fe-7511-11e6-975f-179da21a1997.png)
![uturn](https://cloud.githubusercontent.com/assets/3423533/18378323/eaad3342-7689-11e6-8d20-862fbae5f478.png) 61 | 62 | - **Only-turns** : Only turn-restriction like `only-left-turn`, `only-right-turn` and `only-straight-on` are styled with 63 | - `from` as yellow line. 64 | - `via` as blue dot/line. 65 | - `to` as in blue dashed-line. 66 | ![tt1](https://cloud.githubusercontent.com/assets/3423533/18308794/b46a4576-7513-11e6-879c-b5f85a739080.png) 67 | 68 | 69 | ### Workflow 70 | 71 | #### Step 1 72 | 73 | **Login to OpenStreetMap** : Logging-in to your OpenStreetMap account helps to know who reviewed the detected turn-restriction. 74 | 75 | ![image](https://cloud.githubusercontent.com/assets/3423533/18309190/b8eea1bc-7515-11e6-9f70-1365530f3b27.png) 76 | 77 | #### Step 2 78 | 79 | **Detecting turn-restriction images** : The position of the detected signage will be a probable location of the turn-restriction. 80 | 81 | ![des](https://cloud.githubusercontent.com/assets/3423533/18381733/4ce24852-769b-11e6-8de8-f5065ee967c4.png) 82 | 83 | 1. When clicked on the specific detected signage, you get to edit add/edit the label for the turn-restriction. 84 | 2. The respective images location of detected signage is highlighted as **big** `green arrow`. The highlighted green arrow have the signage in it and lets you to play the sequence of the images to get an overview of where the turn-restriction is applicable when you click on it. 85 | 3. The detected image Mapillary sequence can be played to see the images and get the identify better image with signage. 86 | 4. laying the sequence all the photos present in that sequence are shown as smaller green arrows. We can also open image in a new tab to get a wider picture of the turn restriction by clicking on the image. 87 | 88 | *Note : Sometimes the popup might hide the detected image location. Close the popup to find green-arrows with image location* 89 | 90 | ![nav41](https://cloud.githubusercontent.com/assets/3423533/18381323/563d0e2a-7699-11e6-8006-d129647f8909.gif) 91 | 92 | #### Step 3 93 | 94 | **Add/Verify/Modify turn-restriction** : While navigating in the navigation map using image sequence, we can find out where and to which junction the turn-restriction applies to. 95 | 96 | - Click on `Edit Map` to edit using iD editor - Follow instructions [here](https://github.com/mapbox/mapping/wiki/Mapping-guide-for-adding-turn-restrictions-using-Mapillary#mapping-turn-restrictions-with-id-editor) on how to add turn-restriction in iD editor. 97 | - When clicked on detected signage, the map view bounding box will be downloaded as layer in JOSM (if JOSM is open) - Follow instruction [here](https://github.com/mapbox/mapping/wiki/Mapping-guide-for-adding-turn-restrictions-using-Mapillary#mapping-turn-restrictions-with-josm) on how to add turn-restriction in JOSM editor. 98 | 99 | 100 | #### Step 4 101 | 102 | **Label reviewed turn-restriction** : Once done, update the status of the restriction on the OSM navigation map: 103 | 104 | ![screenshot 2016-09-07 16 52 16](https://cloud.githubusercontent.com/assets/3423533/18310334/57f6d59a-751b-11e6-8f31-a879cb6aaaef.png) 105 | 106 | - `Valid` - The detected restriction is valid and is either already present on OpenStreetMap or added to OpenStreetMap base dont his workflow. 107 | - `Redundant` - The detected restriction is correct but is not necessary to be added to OpenStreetMap since it is redundant for routing. eg. A no left turn against a oneway road. 108 | - `Invalid` - The detected restriction on Mapillary is incorrect. *Do not add to the map* 109 | - `Note` - An OpenStreetMap note can be added if you want any local mapper to verify the turn-restriction before it is added to OpenStreetMap. 110 | 111 | *Note : For junctions where same signage is detected multiple times, review one of them and leave the rest unattended and do not label them with any of the above.* 112 | -------------------------------------------------------------------------------- /assets/pointer-large-focus.svg: -------------------------------------------------------------------------------- 1 | Pointer-1-focus -------------------------------------------------------------------------------- /assets/pointer-large.svg: -------------------------------------------------------------------------------- 1 | Pointer-1 -------------------------------------------------------------------------------- /assets/pointer-small-focus.svg: -------------------------------------------------------------------------------- 1 | Pointer-2-focus -------------------------------------------------------------------------------- /assets/pointer-small.svg: -------------------------------------------------------------------------------- 1 | Pointer-2 -------------------------------------------------------------------------------- /auth.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var osmAuth = require('osm-auth'); 3 | 4 | var auth = osmAuth({ 5 | oauth_secret: '***REMOVED***', 6 | oauth_consumer_key: '***REMOVED***' 7 | }); 8 | 9 | function done(err, res) { 10 | if (err) { 11 | return; 12 | } 13 | var u = res.getElementsByTagName('user')[0]; 14 | var displayName = u.getAttribute('display_name'); 15 | document.getElementById('user').innerHTML = displayName; 16 | document.getElementById('user').style.display = 'block'; 17 | $("#currentReviewer").html(displayName); 18 | } 19 | 20 | function showDetails() { 21 | auth.xhr({ 22 | method: 'GET', 23 | path: '/api/0.6/user/details' 24 | }, done); 25 | } 26 | 27 | function hideDetails() { 28 | document.getElementById('user').innerHTML = ''; 29 | document.getElementById('user').style.display = 'none'; 30 | } 31 | 32 | auth.update = function () { 33 | if (auth.authenticated()) { 34 | document.getElementById('logout').style.display = 'block'; 35 | showDetails(); 36 | } else { 37 | document.getElementById('logout').style.display = 'none'; 38 | hideDetails(); 39 | } 40 | } 41 | 42 | module.exports = auth; 43 | -------------------------------------------------------------------------------- /index.css: -------------------------------------------------------------------------------- 1 | .header { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | text-align: left; 6 | z-index: 6; 7 | margin: 20px; 8 | width: 30%; 9 | } 10 | 11 | .header h3 { 12 | font-size: 18px; 13 | line-height: 20px; 14 | } 15 | 16 | #map { 17 | position: absolute; 18 | height: 100vh; 19 | width: 100vw; 20 | } 21 | 22 | #geocoder-container { 23 | position: absolute; 24 | top: 0; 25 | width: 60%; 26 | margin-top: 10px; 27 | margin-left: 40%; 28 | } 29 | 30 | #geocoder-container > div { 31 | min-width:50%; 32 | margin-left:%; 33 | } 34 | 35 | #sidebar{ 36 | background:rgba(40, 53, 61, 0.23); 37 | } 38 | 39 | .toggles { 40 | width: 320px; 41 | } 42 | 43 | .info { 44 | width: 320px; 45 | color: white; 46 | } 47 | 48 | .fill-darkblue-opaque { 49 | background-color: rgba(34, 59, 83, 0.4); 50 | } 51 | 52 | .button.stroke:hover { 53 | background-color: #3887be !important; 54 | color: rgb(255, 255, 255) !important; 55 | } 56 | 57 | .button.stroke { 58 | box-shadow: 0px 0px 0px 2px #223b53 inset !important; 59 | color: white; 60 | } 61 | 62 | .micro{ 63 | font-weight: bold; 64 | } 65 | 66 | /*Count of overlay features*/ 67 | #feature-stats{ 68 | position: absolute; 69 | bottom: 10px; 70 | right: 10px; 71 | } 72 | #feature-stats ul{ 73 | text-align: right; 74 | } 75 | #feature-stats [data-count-feature]{ 76 | font-weight: bold; 77 | } 78 | #logout { 79 | display: none; 80 | position: absolute; 81 | width: 60px; 82 | margin-top: 20px; 83 | right: 80px; 84 | } 85 | #user { 86 | position: absolute; 87 | display:none; 88 | font:normal 15px/20px 'Helvetica Neue', sans-serif; 89 | text-align:center; 90 | margin-top: 25px; 91 | right: 150px; 92 | color: white; 93 | } 94 | #restrictionValidator { 95 | display:none; 96 | position: absolute; 97 | width: 140px; 98 | bottom: 110px; 99 | right: 10px; 100 | } 101 | /*Mapbox geocoder control style override to fix issue with nav control*/ 102 | .mapboxgl-ctrl-geocoder { 103 | width: 100%; 104 | } 105 | #geocoder-container{ 106 | width:30%; 107 | } 108 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | OpenStreetMap Navigation Data Map 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 36 | 37 |
38 |

OpenStreetMap Navigation Data Map

39 |

Create an open database of navigation data in your city. See instructions on how to use this tool to map turn restrictions.

40 |
41 | 42 |
43 |
44 |
45 |
Logout
46 |
47 |
48 |
49 |
50 |
51 | 62 |
63 |
Restriction Validator
64 |
65 |
    66 |
  • Reviewed
  • 67 |
  • Valid
  • 68 |
  • Redundant
  • 69 |
  • Invalid
  • 70 |
71 |
72 |
73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var DATASETS_PROXY_URL = '***REMOVED***'; 2 | 3 | var MAPBOX_DATA_TEAM = require('mapbox-data-team').getUsernames(); 4 | 5 | var MAPILLARY_CLIENT_ID = "***REMOVED***"; 6 | 7 | var cover = require('tile-cover'); 8 | var turf = require('turf'); 9 | var hat = require('hat'); 10 | 11 | var osmAuth = require('osm-auth'); 12 | var auth = require('./auth'); 13 | auth.update(); 14 | 15 | var queryOverpass = require('query-overpass'); 16 | 17 | var reviewer, mapillaryId, mapillaryImageKey; 18 | 19 | mapboxgl.accessToken = 'pk.eyJ1IjoicGxhbmVtYWQiLCJhIjoiemdYSVVLRSJ9.g3lbg_eN0kztmsfIPxa9MQ'; 20 | 21 | var map = new mapboxgl.Map({ 22 | container: 'map', 23 | style: 'mapbox://styles/planemad/cinpwopfb008hcam0mqxbxwuq', 24 | center: [-105.2, 44.6], 25 | zoom: 3.5, 26 | hash: true, 27 | attributionControl: false, 28 | keyboard: false 29 | }); 30 | 31 | map.addControl(new mapboxgl.Navigation({ 32 | position: 'top-right' 33 | })); 34 | map.addControl(new mapboxgl.Geocoder({ 35 | container: 'geocoder-container' 36 | })); 37 | 38 | var mly = new Mapillary.Viewer('mly', MAPILLARY_CLIENT_ID); 39 | $('#mly').hide(); 40 | 41 | // Layer for review markers 42 | var reviewedRestrictionsSource = new mapboxgl.GeoJSONSource({ 43 | data: {} 44 | }); 45 | var reviewedRestrictions = { 46 | 'id': 'reviewedRestrictions', 47 | 'type': 'circle', 48 | 'source': 'reviewedRestrictionsSource', 49 | 'interactive': true, 50 | 'layout': { 51 | visibility: 'visible' 52 | }, 53 | 'paint': { 54 | 'circle-radius': { 55 | "stops": [ 56 | [5, 1], 57 | [14, 14] 58 | ] 59 | }, 60 | 'circle-blur': { 61 | 'stops': [ 62 | [12, 1], 63 | [14, 0] 64 | ] 65 | }, 66 | 'circle-color': { 67 | 'property': 'status', 68 | 'type': 'categorical', 69 | 'stops': [ 70 | ['valid', '#52a1d8'], 71 | ['redundant', '#fbb03b'], 72 | ['invalid', '#e55e5e'], 73 | ['note', '#8a8acb'] 74 | ] 75 | }, 76 | 'circle-opacity': { 77 | 'stops': [ 78 | [13.9, 1], 79 | [14, 0.5] 80 | ] 81 | } 82 | } 83 | }; 84 | 85 | // Define switchable map layers 86 | var toggleLayers = { 87 | 'turn-restrictions': { 88 | 'layers': ['noturn', 'noturn from', 'noturn via', 'noturn via highlight', 'noturn labels'], 89 | 'title': 'Turn restrictions', 90 | 'description': 'Junctions where a regular turn is prohibited' 91 | }, 92 | 'oneways': { 93 | 'layers': ['oneways', 'oneways arrows'], 94 | 'title': 'Oneway Streets', 95 | 'description': 'Includes oneways segments part of a two way dual carriageway road' 96 | }, 97 | 'traffic-signals': { 98 | 'layers': ['trafficsignals'], 99 | 'title': 'Traffic signals', 100 | 'description': 'Junctions with a street light' 101 | }, 102 | 'turn-lanes': { 103 | 'layers': ['turnlanes'], 104 | 'title': 'Turn lanes', 105 | 'description': 'Streets with turn restricted lanes' 106 | }, 107 | 'maxspeed': { 108 | 'layers': ['maxspeed', 'maxspeed labels'], 109 | 'title': 'Speed limits', 110 | 'description': 'Legal speed limits' 111 | } 112 | }; 113 | 114 | // Define switchable map layer filters 115 | var toggleFilters = { 116 | 'mapbox-team': { 117 | 'filter-mode': 'any', 118 | 'filter-compare': ['==', 'meta_user'], 119 | 'filter-values': MAPBOX_DATA_TEAM 120 | } 121 | } 122 | 123 | // Add Mapillary sprites 124 | // var style = map.getStyle(); 125 | // style.sprite = 'https://www.mapillary.com/sprites/'; 126 | // map.setStyle(style); 127 | 128 | // Map ready 129 | map.on('style.load', function(e) { 130 | init(); 131 | 132 | showOnlyLayers(toggleLayers, null); 133 | 134 | // Highlight only team edits 135 | // toggleLayerFilters('turn-restrictions','mapbox-team'); 136 | 137 | // Toggle map layers list 138 | $('.toggles a').on('click', function(e) { 139 | e.preventDefault(); 140 | var toggleItem = e.target.id.split('#')[0]; 141 | 142 | // Highlight clicked item 143 | $(this).toggleClass('active'); 144 | 145 | // Mapillary overlay toggle 146 | if (toggleItem === 'mapillary') { 147 | toggleMapillary(); 148 | } else if(toggleItem === 'tileBoundaryLayer') { 149 | toggleTileBoundary(); 150 | } else { 151 | var layers = toggleLayers[toggleItem].layers; 152 | for (var i in layers) { 153 | toggle(layers[i]); 154 | } 155 | } 156 | 157 | // Style layers toggle 158 | 159 | // Object.keys(toggleLayers).forEach(function (type) { 160 | // if (type != listId) { 161 | // hide(toggleLayers[type].id); 162 | // } 163 | // }); 164 | 165 | 166 | 167 | // setInfo(toggleItem); 168 | }); 169 | $('#mapillary').click(); // Show Mapillary layer 170 | }); 171 | 172 | function init() { 173 | // do all initialisation stuff here 174 | var mapillaryTrafficSigns = { 175 | "type": "vector", 176 | "tiles": [ 177 | "https://a.mapillary.com/v3/tiles/objects/{z}/{x}/{y}.mvt?layers=mapillary-objects-trafficsigns&client_id=" + MAPILLARY_CLIENT_ID, 178 | ], 179 | "minzoom": 14, 180 | "maxzoom": 17 181 | }; 182 | 183 | var mapillaryCoverage = { 184 | "type": "vector", 185 | "tiles": [ 186 | "https://d25uarhxywzl1j.cloudfront.net/v0.1/{z}/{x}/{y}.mvt" 187 | ], 188 | "maxzoom": 14 189 | }; 190 | 191 | var osmTurnRestrictions = { 192 | "type": "geojson", 193 | "data": { 194 | "type": "FeatureCollection", 195 | "features": [] 196 | } 197 | }; 198 | 199 | var tileBoundarySource = { 200 | "type": "geojson", 201 | "data": "" 202 | }; 203 | 204 | map.addSource("mapillary", mapillaryTrafficSigns); 205 | map.addSource("mapillaryCoverage", mapillaryCoverage); 206 | map.addSource("reviewedRestrictionsSource", reviewedRestrictionsSource); 207 | map.addSource("osmTurnRestrictions", osmTurnRestrictions); 208 | map.addSource("tileBoundarySource", tileBoundarySource); 209 | map.addLayer(reviewedRestrictions); 210 | 211 | // Fetch data every 10 minutes 212 | refreshData(10); 213 | 214 | var mapillaryRestrictionsFilter = [ 215 | "all", 216 | ["in", "value", "regulatory--no-left-turn--g1", "regulatory--no-left-turn--g2", "regulatory--no-right-turn--g1", "regulatory--no-right-turn--g2", "regulatory--no-straight-through--g1", "regulatory--no-u-turn--g1", "regulatory--no-left-or-u-turn--g1", "regulatory--turn-left--g1", "regulatory--turn-right--g1", "regulatory--go-straight--g1", "regulatory--go-straight-or-turn-left--g1", "regulatory--go-straight-or-turn-right--g1", "regulatory--turn-left-ahead--g1", "regulatory--turn-right-ahead--g1"], 217 | [">=", "updated_at", Date.now() - 86400000 * 90], 218 | ]; 219 | 220 | var mapillaryCoverageFilter = [ 221 | ">=", "captured_at", Date.now() - 86400000 * 90 222 | ]; 223 | 224 | var days = location.search; 225 | 226 | if (days) { 227 | days = days.split('=')[1]; 228 | mapillaryRestrictionsFilter[2] = [">=", "updated_at", Date.now() - 86400000 * parseInt(days, 10)]; 229 | mapillaryCoverageFilter = [">=", "captured_at", Date.now() - 86400000 * parseInt(days, 10)]; 230 | } else { 231 | updateUrl('90'); 232 | } 233 | 234 | function updateUrl(days) { 235 | location.search = '?days=' + days; 236 | } 237 | 238 | var mapillaryTraffic = { 239 | "id": "mapillaryTraffic", 240 | "type": "circle", 241 | "source": "mapillary", 242 | 'source-layer': 'mapillary-objects-trafficsigns', 243 | 'layout': { 244 | 'visibility': 'none' 245 | }, 246 | "paint": { 247 | "circle-radius": 2, 248 | "circle-color": "white", 249 | "circle-opacity": { 250 | "stops": [ 251 | [14, 0.5], 252 | [16, 1] 253 | ] 254 | } 255 | } 256 | }; 257 | 258 | var mapillaryTrafficRestrictions = { 259 | "id": "mapillaryTrafficRestrictions", 260 | "type": "circle", 261 | "source": "mapillary", 262 | 'source-layer': 'mapillary-objects-trafficsigns', 263 | 'layout': { 264 | 'visibility': 'none' 265 | }, 266 | "paint": { 267 | "circle-radius": 9, 268 | "circle-color": "hsl(112, 0%, 100%)" 269 | }, 270 | "filter": mapillaryRestrictionsFilter 271 | }; 272 | 273 | var mapillaryCoverageLine = { 274 | "id": "mapillaryCoverageLine", 275 | "type": "line", 276 | "source": "mapillaryCoverage", 277 | "source-layer": "mapillary-sequences", 278 | "layout": { 279 | "visibility": "none" 280 | }, 281 | "paint": { 282 | "line-color": 'white', 283 | "line-width": { 284 | "stops": [ 285 | [8, 1], 286 | [13, 3], 287 | [16, 1] 288 | ] 289 | }, 290 | "line-opacity": { 291 | "stops": [ 292 | [8, 0.2], 293 | [13, 0.7], 294 | [16, 0.4] 295 | ] 296 | } 297 | }, 298 | "filter": mapillaryCoverageFilter 299 | }; 300 | 301 | var mapillaryCoverageLineDirection = { 302 | "id": "mapillaryCoverageLineDirection", 303 | "type": "symbol", 304 | "source": "mapillaryCoverage", 305 | "source-layer": "mapillary-sequences", 306 | "layout": { 307 | "icon-image": "mapillary-direction", 308 | "icon-rotation-alignment": "map", 309 | "icon-keep-upright": false, 310 | "symbol-spacing": 100, 311 | "icon-rotate": 90, 312 | "icon-allow-overlap": false, 313 | "symbol-placement": "line", 314 | "visibility": "none", 315 | "icon-size": { 316 | "base": 1, 317 | "stops": [ 318 | [ 319 | 14, 320 | 0 321 | ], 322 | [ 323 | 16, 324 | 1 325 | ] 326 | ] 327 | }, 328 | "icon-ignore-placement": false 329 | }, 330 | "paint": { 331 | "icon-opacity": 1 332 | }, 333 | "filter": mapillaryCoverageFilter 334 | }; 335 | 336 | var mapillaryTrafficHighlight = { 337 | "id": "mapillaryTrafficHighlight", 338 | "type": "circle", 339 | "source": "mapillary", 340 | 'source-layer': 'mapillary-objects-trafficsigns', 341 | "layout": { 342 | "visibility": "none" 343 | }, 344 | "paint": { 345 | "circle-radius": 15, 346 | "circle-opacity": 0.3, 347 | "circle-color": "white" 348 | }, 349 | "filter": ["==", "key", ""] 350 | }; 351 | 352 | var mapillaryTrafficLabel = { 353 | "id": "mapillaryTrafficLabel", 354 | "type": "symbol", 355 | "source": "mapillary", 356 | "source-layer": "mapillary-objects-trafficsigns", 357 | "layout": { 358 | "text-field": "{value}", 359 | "text-size": 8, 360 | "text-offset": [0, 2], 361 | "visibility": "none" 362 | }, 363 | "paint": { 364 | "text-color": "white", 365 | "text-halo-color": "black", 366 | "text-halo-width": 1, 367 | "text-opacity": { 368 | "stops": [ 369 | [14, 0], 370 | [18, 1] 371 | ] 372 | } 373 | } 374 | }; 375 | 376 | var mapillaryTrafficRestrictionsIcon = { 377 | "id": "mapillaryTrafficRestrictionsIcon", 378 | "type": "symbol", 379 | "source": "mapillary", 380 | "source-layer": "mapillary-objects-trafficsigns", 381 | "layout": { 382 | "icon-image": "{value}", 383 | 'icon-image': '{value}', 384 | 'icon-allow-overlap': true, 385 | 'icon-size': 0.8, 386 | 'visibility': 'none' 387 | }, 388 | "filter": mapillaryRestrictionsFilter 389 | }; 390 | 391 | var mapillaryTrafficRestrictionsLabel = { 392 | "id": "mapillaryTrafficRestrictionsLabel", 393 | "type": "symbol", 394 | "source": "mapillary", 395 | "source-layer": "mapillary-objects-trafficsigns", 396 | "layout": { 397 | "text-field": "{value}", 398 | "text-size": 14, 399 | "text-offset": [0, 2], 400 | "text-font": ["Clan Offc Pro Bold"], 401 | "visibility": "none", 402 | 'text-allow-overlap': false 403 | }, 404 | "paint": { 405 | "text-color": "hsl(112, 100%, 50%)", 406 | "text-halo-color": "black", 407 | "text-halo-width": 1, 408 | "text-opacity": { 409 | "stops": [ 410 | [15, 0], 411 | [16, 1] 412 | ] 413 | } 414 | }, 415 | "filter": mapillaryRestrictionsFilter 416 | }; 417 | 418 | var mapillaryImages = { 419 | 'id': 'mapillaryImages', 420 | 'type': 'symbol', 421 | 'source': 'mapillaryCoverage', 422 | 'source-layer': 'mapillary-images', 423 | 'layout': { 424 | 'visibility': 'visible', 425 | 'icon-image': 'Pointer-1', 426 | 'icon-rotate': { 427 | 'property': 'ca', 428 | 'stops': iconRotations() 429 | } 430 | }, 431 | 'filter': ['==', 'key', ''] 432 | }; 433 | 434 | var mapillaryImagesHighlight = { 435 | 'id': 'mapillaryImagesHighlight', 436 | 'type': 'symbol', 437 | 'source': 'mapillaryCoverage', 438 | 'source-layer': 'mapillary-images', 439 | 'layout': { 440 | 'visibility': 'visible', 441 | 'icon-image': 'Pointer-1-focus', 442 | 'icon-rotate': { 443 | 'property': 'ca', 444 | 'stops': iconRotations() 445 | } 446 | }, 447 | 'filter': ['==', 'key', ''] 448 | }; 449 | 450 | var mapillarySequence = { 451 | 'id': 'mapillarySequence', 452 | 'type': 'symbol', 453 | 'source': 'mapillaryCoverage', 454 | 'source-layer': 'mapillary-images', 455 | 'layout': { 456 | 'visibility': 'visible', 457 | 'icon-image': 'Pointer-2', 458 | 'icon-rotate': { 459 | 'property': 'ca', 460 | 'stops': iconRotations() 461 | } 462 | }, 463 | 'filter': ['==', 'skey', ''] 464 | }; 465 | 466 | var mapillarySequenceHighlight = { 467 | 'id': 'mapillarySequenceHighlight', 468 | 'type': 'symbol', 469 | 'source': 'mapillaryCoverage', 470 | 'source-layer': 'mapillary-images', 471 | 'layout': { 472 | 'visibility': 'visible', 473 | 'icon-image': 'Pointer-2-focus', 474 | 'icon-rotate': { 475 | 'property': 'ca', 476 | 'stops': iconRotations() 477 | } 478 | }, 479 | 'filter': ['==', 'key', ''] 480 | }; 481 | 482 | var osmNoStraightFrom = { 483 | 'id': 'no-straight-from', 484 | 'type': 'line', 485 | 'source': 'osmTurnRestrictions', 486 | "filter": [ 487 | "all", 488 | [ 489 | "==", 490 | "$type", 491 | "LineString" 492 | ], 493 | [ 494 | "==", 495 | "relations_role", 496 | "from" 497 | ] 498 | ], 499 | "layout": { 500 | "visibility": "visible", 501 | "line-cap": "butt", 502 | "line-join": "round" 503 | }, 504 | "paint": { 505 | "line-color": "hsl(49, 71%, 58%)", 506 | "line-opacity": { 507 | "base": 1, 508 | "stops": [ 509 | [ 510 | 14.9, 511 | 0 512 | ], 513 | [ 514 | 15.1, 515 | 0.5 516 | ] 517 | ] 518 | }, 519 | "line-width": 4 520 | } 521 | }; 522 | 523 | var osmNoStraightTo = { 524 | 'id': 'no-straight-to', 525 | 'type': 'line', 526 | 'source': 'osmTurnRestrictions', 527 | "filter": [ 528 | "all", 529 | [ 530 | "==", 531 | "$type", 532 | "LineString" 533 | ], 534 | [ 535 | "all", 536 | [ 537 | "==", 538 | "relations_role", 539 | "to" 540 | ], 541 | [ 542 | "in", 543 | "relations_reltags_restriction", 544 | "no_left_turn", 545 | "no_right_turn", 546 | "no_straight_on", 547 | "no_u_turn" 548 | ] 549 | ] 550 | ], 551 | "layout": { 552 | "visibility": "visible", 553 | "line-cap": "round", 554 | "line-join": "round" 555 | }, 556 | "paint": { 557 | "line-color": "hsl(0, 91%, 55%)", 558 | "line-opacity": { 559 | "base": 1, 560 | "stops": [ 561 | [ 562 | 14.9, 563 | 0 564 | ], 565 | [ 566 | 15.1, 567 | 0.7 568 | ] 569 | ] 570 | }, 571 | "line-dasharray": { 572 | "base": 1, 573 | "stops": [ 574 | [ 575 | 15, 576 | [ 577 | 1, 578 | 2 579 | ] 580 | ], 581 | [ 582 | 18, 583 | [ 584 | 4, 585 | 4 586 | ] 587 | ] 588 | ] 589 | }, 590 | "line-width": 2 591 | } 592 | }; 593 | 594 | var osmOnlyTo = { 595 | 'id': 'only-to', 596 | 'type': 'line', 597 | 'source': 'osmTurnRestrictions', 598 | "filter": [ 599 | "all", 600 | [ 601 | "==", 602 | "$type", 603 | "LineString" 604 | ], 605 | [ 606 | "all", 607 | [ 608 | "==", 609 | "relations_role", 610 | "to" 611 | ], 612 | [ 613 | "in", 614 | "relations_reltags_restriction", 615 | "only_left_turn", 616 | "only_right_turn", 617 | "only_straight_on" 618 | ] 619 | ] 620 | ], 621 | "layout": { 622 | "visibility": "visible", 623 | "line-cap": "round", 624 | "line-join": "round" 625 | }, 626 | "paint": { 627 | "line-color": "hsl(206, 100%, 50%)", 628 | "line-opacity": { 629 | "base": 1, 630 | "stops": [ 631 | [ 632 | 14.9, 633 | 0 634 | ], 635 | [ 636 | 15.1, 637 | 0.7 638 | ] 639 | ] 640 | }, 641 | "line-width": 3, 642 | "line-dasharray": { 643 | "base": 1, 644 | "stops": [ 645 | [ 646 | 15, 647 | [ 648 | 3, 649 | 1 650 | ] 651 | ], 652 | [ 653 | 18, 654 | [ 655 | 6, 656 | 2 657 | ] 658 | ] 659 | ] 660 | } 661 | } 662 | }; 663 | 664 | var osmToConditional = { 665 | 'id': 'to-conditional', 666 | 'type': 'line', 667 | 'source': 'osmTurnRestrictions', 668 | "filter": [ 669 | "all", 670 | [ 671 | "==", 672 | "$type", 673 | "LineString" 674 | ], 675 | [ 676 | "all", 677 | [ 678 | "!has", 679 | "relations_reltags_restriction" 680 | ], 681 | [ 682 | "==", 683 | "relations_role", 684 | "to" 685 | ] 686 | ] 687 | ], 688 | "layout": { 689 | "visibility": "visible", 690 | "line-cap": "round", 691 | "line-join": "round" 692 | }, 693 | "paint": { 694 | "line-color": "hsl(0, 91%, 55%)", 695 | "line-opacity": { 696 | "base": 1, 697 | "stops": [ 698 | [ 699 | 14.9, 700 | 0 701 | ], 702 | [ 703 | 15.1, 704 | 0.7 705 | ] 706 | ] 707 | }, 708 | "line-dasharray": { 709 | "base": 1, 710 | "stops": [ 711 | [ 712 | 15, 713 | [ 714 | 1, 715 | 2 716 | ] 717 | ], 718 | [ 719 | 18, 720 | [ 721 | 4, 722 | 4 723 | ] 724 | ] 725 | ] 726 | }, 727 | "line-width": 2 728 | } 729 | }; 730 | 731 | var osmNoUVia = { 732 | 'id': 'no-u-via', 733 | 'type': 'line', 734 | 'source': 'osmTurnRestrictions', 735 | "filter": [ 736 | "all", 737 | [ 738 | "==", 739 | "$type", 740 | "LineString" 741 | ], 742 | [ 743 | "==", 744 | "relations_role", 745 | "via" 746 | ] 747 | ], 748 | "layout": { 749 | "visibility": "visible", 750 | "line-cap": "round", 751 | "line-join": "round" 752 | }, 753 | "paint": { 754 | "line-color": "hsl(29, 100%, 50%)", 755 | "line-opacity": { 756 | "base": 1, 757 | "stops": [ 758 | [ 759 | 14.9, 760 | 0 761 | ], 762 | [ 763 | 15.1, 764 | 0.7 765 | ] 766 | ] 767 | }, 768 | "line-dasharray": [ 769 | 1, 770 | 2 771 | ], 772 | "line-width": 3 773 | } 774 | }; 775 | 776 | var osmNoStraightViaNodes = { 777 | 'id': 'no-straight-via-nodes', 778 | 'type': 'circle', 779 | 'source': 'osmTurnRestrictions', 780 | "filter": [ 781 | "all", 782 | [ 783 | "==", 784 | "$type", 785 | "Point" 786 | ], 787 | [ 788 | "all", 789 | [ 790 | "==", 791 | "relations_role", 792 | "via" 793 | ], 794 | [ 795 | "in", 796 | "relations_reltags_restriction", 797 | "no_left_turn", 798 | "no_right_turn", 799 | "no_straight_on", 800 | "no_u_turn" 801 | ] 802 | ] 803 | ], 804 | "layout": { 805 | "visibility": "visible" 806 | }, 807 | "paint": { 808 | "circle-color": "hsl(0, 66%, 53%)", 809 | "circle-opacity": { 810 | "base": 1, 811 | "stops": [ 812 | [ 813 | 13.9, 814 | 0 815 | ], 816 | [ 817 | 14, 818 | 0.6 819 | ] 820 | ] 821 | }, 822 | "circle-radius": { 823 | "base": 1, 824 | "stops": [ 825 | [ 826 | 10, 827 | 4 828 | ], 829 | [ 830 | 13.9, 831 | 6 832 | ], 833 | [ 834 | 14, 835 | 5 836 | ] 837 | ] 838 | } 839 | } 840 | }; 841 | 842 | var osmOnlyViaNodes = { 843 | 'id': 'only-via-nodes', 844 | 'type': 'circle', 845 | 'source': 'osmTurnRestrictions', 846 | "filter": [ 847 | "all", 848 | [ 849 | "==", 850 | "$type", 851 | "Point" 852 | ], 853 | [ 854 | "all", 855 | [ 856 | "==", 857 | "relations_role", 858 | "via" 859 | ], 860 | [ 861 | "in", 862 | "relations_reltags_restriction", 863 | "only_left_turn", 864 | "only_right_turn", 865 | "only_straight_on" 866 | ] 867 | ] 868 | ], 869 | "layout": { 870 | "visibility": "visible" 871 | }, 872 | "paint": { 873 | "circle-color": "hsl(206, 100%, 50%)", 874 | "circle-opacity": { 875 | "base": 1, 876 | "stops": [ 877 | [ 878 | 13.9, 879 | 0 880 | ], 881 | [ 882 | 14, 883 | 0.6 884 | ] 885 | ] 886 | }, 887 | "circle-radius": { 888 | "base": 1, 889 | "stops": [ 890 | [ 891 | 10, 892 | 4 893 | ], 894 | [ 895 | 13.9, 896 | 6 897 | ], 898 | [ 899 | 14, 900 | 5 901 | ] 902 | ] 903 | } 904 | } 905 | }; 906 | 907 | var osmViaNodesConditional = { 908 | 'id': 'via-nodes-conditional', 909 | 'type': 'circle', 910 | 'source': 'osmTurnRestrictions', 911 | "filter": [ 912 | "all", 913 | [ 914 | "==", 915 | "$type", 916 | "Point" 917 | ], 918 | [ 919 | "all", 920 | [ 921 | "!has", 922 | "relations_reltags_restriction" 923 | ], 924 | [ 925 | "==", 926 | "relations_role", 927 | "via" 928 | ] 929 | ] 930 | ], 931 | "layout": { 932 | "visibility": "visible" 933 | }, 934 | "paint": { 935 | "circle-color": "hsl(0, 66%, 53%)", 936 | "circle-opacity": { 937 | "base": 1, 938 | "stops": [ 939 | [ 940 | 12, 941 | 0.4 942 | ], 943 | [ 944 | 14, 945 | 0.6 946 | ] 947 | ] 948 | }, 949 | "circle-radius": { 950 | "base": 1, 951 | "stops": [ 952 | [ 953 | 10, 954 | 4 955 | ], 956 | [ 957 | 13.9, 958 | 6 959 | ], 960 | [ 961 | 14, 962 | 5 963 | ] 964 | ] 965 | } 966 | } 967 | }; 968 | 969 | var tileBoundaryLayer = { 970 | 'id': 'tileBoundaryLayer', 971 | 'type': 'line', 972 | 'source': 'tileBoundarySource', 973 | 'layout': { 974 | 'visibility': 'none', 975 | }, 976 | "paint": { 977 | "line-color": 'red', 978 | } 979 | }; 980 | 981 | var tileBoundaryTextLayer = { 982 | 'id': 'tileBoundaryTextLayer', 983 | 'type': 'symbol', 984 | 'source': 'tileBoundarySource', 985 | 'layout': { 986 | 'visibility': 'none', 987 | "text-field": "{id}", 988 | "text-size": 14, 989 | "text-offset": [4, 4], 990 | }, 991 | "paint": { 992 | "text-color": 'red', 993 | } 994 | }; 995 | 996 | map.addLayer(mapillaryCoverageLine, 'noturn'); 997 | map.addLayer(mapillaryCoverageLineDirection); 998 | 999 | map.addLayer(osmNoStraightFrom); 1000 | map.addLayer(osmNoStraightTo); 1001 | map.addLayer(osmOnlyTo); 1002 | map.addLayer(osmToConditional); 1003 | map.addLayer(osmNoUVia); 1004 | map.addLayer(osmNoStraightViaNodes); 1005 | map.addLayer(osmOnlyViaNodes); 1006 | map.addLayer(osmViaNodesConditional); 1007 | 1008 | map.addLayer(mapillaryTrafficHighlight); 1009 | map.addLayer(mapillaryTrafficLabel); 1010 | map.addLayer(mapillaryTrafficRestrictions, 'noturn'); 1011 | map.addLayer(mapillaryTraffic, 'noturn'); 1012 | map.addLayer(mapillaryTrafficRestrictionsIcon); 1013 | map.addLayer(mapillaryTrafficRestrictionsLabel); 1014 | 1015 | map.addLayer(mapillaryImages); 1016 | map.addLayer(mapillaryImagesHighlight); 1017 | map.addLayer(mapillarySequence, 'mapillaryImages'); 1018 | map.addLayer(mapillarySequenceHighlight, 'mapillaryImagesHighlight'); 1019 | 1020 | map.addLayer(tileBoundaryLayer, 'tileBoundaryLayer'); 1021 | map.addLayer(tileBoundaryTextLayer,'tileBoundaryTextLayer'); 1022 | 1023 | document.getElementById('logout').onclick = function() { 1024 | auth.logout(); 1025 | auth.update(); 1026 | }; 1027 | 1028 | updateRestrictionValidator(); 1029 | map.on('moveend', function () { 1030 | updateRestrictionValidator(); 1031 | var tileBoundarybutton = document.getElementById('tileBoundaryLayer'); 1032 | if (map.getZoom() >= 10 && tileBoundarybutton.classList.contains('active')) { 1033 | showTileBoundary(); 1034 | } else { 1035 | hideTileBoundary(); 1036 | } 1037 | }); 1038 | 1039 | function updateRestrictionValidator() { 1040 | var zoom = Math.round(map.getZoom()); 1041 | var lat = map.getCenter().lat; 1042 | var lng = map.getCenter().lng; 1043 | if (zoom > 14) { 1044 | document.getElementById('restrictionValidator').style.display = 'block'; 1045 | document.getElementById('restrictionValidator').onclick = function () { 1046 | window.open('http://restrictions.morbz.de/#' + zoom + '/' + lat + '/' + lng); 1047 | }; 1048 | } else { 1049 | document.getElementById('restrictionValidator').style.display = 'none'; 1050 | } 1051 | } 1052 | 1053 | map.on('click', function(e) { 1054 | var mapillaryRestrictions = map.queryRenderedFeatures([ 1055 | [e.point.x - 5, e.point.y - 5], 1056 | [e.point.x + 5, e.point.y + 5] 1057 | ], { 1058 | layers: ['mapillaryTraffic'] 1059 | }); 1060 | 1061 | 1062 | if (mapillaryRestrictions.length) { 1063 | var restriction = mapillaryRestrictions[0]; 1064 | 1065 | var detections = restriction.properties.detections; 1066 | var imageKeys = JSON.parse(detections).map(function(detections) { 1067 | return detections.image_key; 1068 | }); 1069 | 1070 | var restrictionKey = restriction.properties.key; 1071 | 1072 | map.setFilter('mapillaryTrafficHighlight', ['==', 'key', restrictionKey]); 1073 | map.setFilter('mapillaryImages', ['in', 'key'].concat(imageKeys)); 1074 | map.setFilter('mapillaryImagesHighlight', ['==', 'key', '']); 1075 | map.setFilter('mapillarySequence', ['==', 'skey', '']); 1076 | map.setFilter('mapillarySequenceHighlight', ['==', 'key', '']); 1077 | 1078 | $('#mly').hide(); 1079 | 1080 | openInJOSM(); 1081 | } 1082 | 1083 | var mapillaryImages = map.queryRenderedFeatures([ 1084 | [e.point.x - 5, e.point.y - 5], 1085 | [e.point.x + 5, e.point.y + 5] 1086 | ], { 1087 | layers: ['mapillaryImages'] 1088 | }); 1089 | 1090 | if (mapillaryImages.length) { 1091 | var image = mapillaryImages[0]; 1092 | var imageKey = image.properties.key; 1093 | var sequenceKey = image.properties.skey; 1094 | 1095 | mapillaryId = imageKey; 1096 | mapillaryImageKey = imageKey; 1097 | 1098 | map.setFilter('mapillaryImagesHighlight', ['==', 'key', image.properties.key]); 1099 | 1100 | $('#mly').show(); 1101 | mly.moveToKey(imageKey); 1102 | } 1103 | 1104 | var mapillarySequence = map.queryRenderedFeatures([ 1105 | [e.point.x - 5, e.point.y - 5], 1106 | [e.point.x + 5, e.point.y + 5] 1107 | ], { 1108 | layers: ['mapillarySequence'] 1109 | }); 1110 | 1111 | if (mapillarySequence.length) { 1112 | var image = mapillarySequence[0]; 1113 | var imageKey = image.properties.key; 1114 | 1115 | mapillaryImageKey = imageKey; 1116 | 1117 | map.setFilter('mapillarySequenceHighlight', ['==', 'key', image.properties.key]); 1118 | 1119 | mly.moveToKey(imageKey); 1120 | } 1121 | 1122 | // Show popup of OSM feature 1123 | var osmFeature = map.queryRenderedFeatures(e.point, { 1124 | layers: ['noturn'] 1125 | }); 1126 | if (osmFeature.length) { 1127 | 1128 | // Check if feature is node or a way 1129 | var osmType = osmFeature[0].geometry.type == 'LineString' ? 'way' : 'node'; 1130 | var point = osmType == 'way' ? e.lngLat : osmFeature[0].geometry.coordinates; 1131 | var popupHTML = "OpenStreetMap Turn Restriction
" + osmType + ": " + osmFeature[0].properties.id + "" 1132 | 1133 | var popup = new mapboxgl.Popup() 1134 | .setLngLat(point) 1135 | .setHTML(popupHTML) 1136 | .addTo(map); 1137 | openInJOSM(); 1138 | } 1139 | 1140 | // Add review marker 1141 | var newfeaturesGeoJSON = { 1142 | "type": "Feature", 1143 | "properties": {}, 1144 | "geometry": { 1145 | "coordinates": [ 1146 | 1147 | ], 1148 | "type": "Point" 1149 | }, 1150 | 1151 | }; 1152 | 1153 | // Create review popup if there is a mapillary restriction nearby 1154 | if (mapillaryRestrictions.length) { 1155 | 1156 | var restriction = mapillaryRestrictions[0].properties.value; 1157 | 1158 | if (!featuresGeoJSON.features.length) { 1159 | console.log("This is an empty dataset"); 1160 | reviewFeature(); 1161 | 1162 | } else { 1163 | 1164 | var reviewedFeatures = map.queryRenderedFeatures(e.point, { 1165 | layers: ['reviewedRestrictions'] 1166 | }); 1167 | if (reviewedFeatures.length) { 1168 | reviewFeature(reviewedFeatures[0]); 1169 | 1170 | // var popupHTML = "

" + restriction + "

" + "already reviewed as " + reviewedFeatures[0].properties["status"]; 1171 | // var popup = new mapboxgl.Popup() 1172 | // .setLngLat(e.lngLat) 1173 | // .setHTML(popupHTML) 1174 | // .addTo(map); 1175 | 1176 | } else { 1177 | reviewFeature(); 1178 | } 1179 | } 1180 | 1181 | function reviewFeature(feature) { 1182 | var formOptions = "
"; 1183 | var formReviewer; 1184 | var popupHTML; 1185 | if (auth.authenticated()) { 1186 | formReviewer = "
"; 1187 | popupHTML = "

" + restriction + " Edit Map

" + formOptions + formReviewer + "SaveDelete
"; 1188 | } else { 1189 | formReviewer = "
"; 1190 | popupHTML = "

" + restriction + " Edit Map

" + formOptions + formReviewer + "Login
"; 1191 | } 1192 | 1193 | var popup = new mapboxgl.Popup() 1194 | .setLngLat(e.lngLat) 1195 | .setHTML(popupHTML) 1196 | .addTo(map); 1197 | 1198 | // Show existing status if available 1199 | if (feature) { 1200 | $("input[name=review][value=" + feature.properties["status"] + "]").prop('checked', true); 1201 | $("#reviewer").html(feature.properties["reviewed_by"]); 1202 | newfeaturesGeoJSON = feature; 1203 | newfeaturesGeoJSON["id"] = feature.properties["id"]; 1204 | console.log(feature); 1205 | } else { 1206 | newfeaturesGeoJSON.properties["name"] = restriction; 1207 | newfeaturesGeoJSON.geometry.coordinates = e.lngLat.toArray(); 1208 | } 1209 | 1210 | if (auth.authenticated()) { 1211 | var userName = document.getElementById('user').innerHTML; 1212 | $("#currentReviewer").html(userName); 1213 | 1214 | // Update dataset with feature status on clicking save 1215 | document.getElementById("saveReview").onclick = function() { 1216 | newfeaturesGeoJSON.properties["status"] = $("input[name=review]:checked").val(); 1217 | reviewer = $("input[name=reviewer]").val(); 1218 | newfeaturesGeoJSON.properties["reviewed_by"] = reviewer; 1219 | newfeaturesGeoJSON.properties["reviewed_on"] = Date.now(); 1220 | newfeaturesGeoJSON.properties["mapillary_id"] = mapillaryId; 1221 | popup.remove(); 1222 | newfeaturesGeoJSON["id"] = newfeaturesGeoJSON["id"] || hat(); 1223 | $.ajax({ 1224 | url: DATASETS_PROXY_URL + "/" + newfeaturesGeoJSON["id"], 1225 | type: "PUT", 1226 | contentType: "application/json", 1227 | data: JSON.stringify(newfeaturesGeoJSON) 1228 | }).done(function(response) { 1229 | featuresGeoJSON.features = featuresGeoJSON.features.concat(response); 1230 | reviewedRestrictionsSource.setData(featuresGeoJSON); 1231 | }); 1232 | }; 1233 | // Delete feature on clicking delete 1234 | document.getElementById("deleteReview").onclick = function() { 1235 | popup.remove(); 1236 | $.ajax({ 1237 | url: DATASETS_PROXY_URL + "/" + newfeaturesGeoJSON["id"], 1238 | type: "DELETE" 1239 | }) 1240 | }; 1241 | 1242 | document.getElementById('logout').onclick = function() { 1243 | auth.logout(); 1244 | auth.update(); 1245 | popup.remove(); 1246 | }; 1247 | } else { 1248 | document.getElementById("authenticate").onclick = function() { 1249 | auth.authenticate(function() { 1250 | auth.update(); 1251 | if (auth.authenticated) { 1252 | popup.remove(); 1253 | reviewFeature(feature); 1254 | } 1255 | }); 1256 | }; 1257 | } 1258 | } 1259 | } 1260 | }); 1261 | 1262 | map.on('load', function() { 1263 | // Set feature counts 1264 | var turnrestrictionsCount = map.querySourceFeatures('composite', { 1265 | 'sourceLayer': 'turnrestrictions' 1266 | }).length; 1267 | var onewayCount = map.querySourceFeatures('composite', { 1268 | 'sourceLayer': 'road' 1269 | }).length; 1270 | var turnlanesCount = map.querySourceFeatures('composite', { 1271 | 'sourceLayer': 'turnlanes' 1272 | }).length; 1273 | var trafficsignalsCount = map.querySourceFeatures('composite', { 1274 | 'sourceLayer': 'trafficsignals' 1275 | }).length; 1276 | var speedCount = map.querySourceFeatures('composite', { 1277 | 'sourceLayer': 'maxspeed' 1278 | }).length; 1279 | var mapillaryphotoCount = map.querySourceFeatures('mapillary', { 1280 | 'sourceLayer': 'objects' 1281 | }).length; 1282 | 1283 | $('#turn-restriction-count').text(Math.floor(turnrestrictionsCount / 3)); 1284 | $('#oneway-count').text(onewayCount); 1285 | $('#turn-lanes-count').text(turnlanesCount); 1286 | $('#traffic-signals-count').text(trafficsignalsCount); 1287 | $('#maxspeed-count').text(speedCount); 1288 | }); 1289 | 1290 | 1291 | mly.on(Mapillary.Viewer.nodechanged, function(node) { 1292 | map.setFilter('mapillarySequence', ['==', 'skey', node.sequenceKey]); 1293 | map.setFilter('mapillarySequenceHighlight', ['==', 'key', node.key]); 1294 | 1295 | mapillaryImageKey = node.key; 1296 | }); 1297 | 1298 | map.on('dragend', function(event) { 1299 | if (map.getZoom() > 14) { 1300 | getTurnRestrictions(function(error, data) { 1301 | if (error) return console.error(error); 1302 | map.getSource('osmTurnRestrictions').setData(data); 1303 | }); 1304 | } 1305 | }); 1306 | 1307 | map.on('zoomend', function(event) { 1308 | if (map.getZoom() > 14) { 1309 | getTurnRestrictions(function(error, data) { 1310 | if (error) return console.error(error); 1311 | map.getSource('osmTurnRestrictions').setData(data); 1312 | }) 1313 | } 1314 | }); 1315 | } 1316 | 1317 | 1318 | function toggleMapillary() { 1319 | var mapillaryLayers = ['mapillaryCoverageLine', 'mapillaryCoverageLineDirection', 'mapillaryTrafficHighlight', 'mapillaryTraffic', 'mapillaryTrafficRestrictions', 'mapillaryTrafficLabel', 'mapillaryTrafficRestrictionsIcon', 'mapillaryTrafficRestrictionsLabel']; 1320 | 1321 | mapillaryLayers.forEach(function(id) { 1322 | var currentState = map.getLayoutProperty(id, 'visibility'); 1323 | var nextState = currentState === 'none' ? 'visible' : 'none'; 1324 | map.setLayoutProperty(id, 'visibility', nextState); 1325 | }); 1326 | // if (!$("#mapillary-image").hasClass('hidden')) { 1327 | // $("#mapillary-image").addClass('hidden'); 1328 | // } 1329 | } 1330 | 1331 | function toggleTileBoundary() { 1332 | var tileBoundarybutton = document.getElementById('tileBoundaryLayer'); 1333 | if (tileBoundarybutton.classList.contains('active') && map.getZoom() >= 10) { 1334 | showTileBoundary(); 1335 | } else { 1336 | hideTileBoundary(); 1337 | } 1338 | } 1339 | 1340 | function showTileBoundary() { 1341 | map.setLayoutProperty('tileBoundaryLayer', 'visibility', 'visible'); 1342 | map.setLayoutProperty('tileBoundaryTextLayer', 'visibility', 'visible'); 1343 | var bbox = [map.getBounds()["_sw"]["lng"], map.getBounds()["_sw"]["lat"], map.getBounds()["_ne"]["lng"], map.getBounds()["_ne"]["lat"]]; 1344 | var poly = turf.bboxPolygon(bbox); 1345 | var limits = { 1346 | min_zoom: 14, 1347 | max_zoom: 14 1348 | }; 1349 | 1350 | var geojson = cover.geojson(poly.geometry, limits); 1351 | var indexes = []; 1352 | indexes = cover.tiles(poly.geometry, limits); 1353 | var arr = geojson.features; 1354 | var i; 1355 | 1356 | for(i = 0; i < geojson.features.length; i++) { 1357 | geojson.features[i]["properties"] = {'id' : indexes[i][0] + ',' + indexes[i][1]}; 1358 | } 1359 | console.log(geojson); 1360 | map.getSource('tileBoundarySource').setData(geojson); 1361 | } 1362 | 1363 | function hideTileBoundary() { 1364 | map.setLayoutProperty('tileBoundaryLayer', 'visibility', 'none'); 1365 | map.setLayoutProperty('tileBoundaryTextLayer', 'visibility', 'none'); 1366 | } 1367 | 1368 | // Get data from a Mapbox dataset 1369 | 1370 | var featuresGeoJSON = { 1371 | 'type': 'FeatureCollection', 1372 | 'features': [] 1373 | }; 1374 | 1375 | function refreshData(refreshRate) { 1376 | featuresGeoJSON = { 1377 | 'type': 'FeatureCollection', 1378 | 'features': [] 1379 | }; 1380 | 1381 | getFeatures(); 1382 | 1383 | // setInterval(function(){ getFeatures(); }, 3000); 1384 | 1385 | function getFeatures(startID) { 1386 | 1387 | var url = DATASETS_PROXY_URL + (startID ? '?start=' + startID : ''); 1388 | 1389 | $.getJSON(url, function(data) { 1390 | 1391 | console.log(data); 1392 | 1393 | if (data.features.length) { 1394 | data.features.forEach(function(feature) { 1395 | // Add dataset feature id as a property 1396 | feature.properties.id = feature.id; 1397 | }); 1398 | featuresGeoJSON.features = featuresGeoJSON.features.concat(data.features); 1399 | var lastFeatureID = data.features[data.features.length - 1].id; 1400 | getFeatures(lastFeatureID); 1401 | reviewedRestrictionsSource.setData(featuresGeoJSON); 1402 | } 1403 | reviewedRestrictionsSource.setData(featuresGeoJSON); 1404 | 1405 | var stats = countProperty(featuresGeoJSON, 'status'); 1406 | // Update counts in the page 1407 | for (var prop in stats) { 1408 | $('[data-count-feature="' + prop + '"]').html(stats[prop]); 1409 | } 1410 | }); 1411 | } 1412 | 1413 | } 1414 | 1415 | 1416 | 1417 | // Toggle visibility of a layer 1418 | function toggle(id) { 1419 | var currentState = map.getLayoutProperty(id, 'visibility'); 1420 | var nextState = currentState === 'none' ? 'visible' : 'none'; 1421 | map.setLayoutProperty(id, 'visibility', nextState); 1422 | } 1423 | 1424 | // Show only a specific group of layers 1425 | function showOnlyLayers(toggleLayers, showLayerItem) { 1426 | for (var layerItem in toggleLayers) { 1427 | for (var layer in toggleLayers[layerItem].layers) { 1428 | if (showLayerItem == layerItem) 1429 | map.setLayoutProperty(toggleLayers[layerItem].layers[layer], 'visibility', 'visible'); 1430 | else 1431 | map.setLayoutProperty(toggleLayers[layerItem].layers[layer], 'visibility', 'none'); 1432 | } 1433 | } 1434 | // Highlight menu items 1435 | $('.toggles a').removeClass('active'); 1436 | $('#' + showLayerItem).addClass('active'); 1437 | } 1438 | 1439 | 1440 | 1441 | // Toggle a set of filters for a set of layers 1442 | function toggleLayerFilters(layerItems, filterItem) { 1443 | 1444 | for (var i in layerItems) { 1445 | for (var j in toggleLayers[layerItems[i]].layers) { 1446 | 1447 | var existingFilter = map.getFilter(toggleLayers[layerItems[i]].layers[j]); 1448 | 1449 | // Construct and add the filters if none exist for the layers 1450 | if (typeof existingFilter == 'undefined') { 1451 | map.setFilter(toggleLayers[layerItems[i]].layers[j], toggleFilters[filterItem].filter); 1452 | } else { 1453 | // Not implemented 1454 | var newFilter = mergeLayerFilters(existingFilter, toggleFilters[filterItem].filter); 1455 | map.setFilter(toggleLayers[layerItems[i]].layers[j], newFilter); 1456 | // console.log(newFilter); 1457 | } 1458 | 1459 | } 1460 | } 1461 | } 1462 | 1463 | // Merge two GL layer filters into one 1464 | function mergeLayerFilters(existingFilter, mergeFilter) { 1465 | var newFilter = new Array(); 1466 | 1467 | // If the layer has a simple single filter 1468 | if (existingFilter[0] == '==') { 1469 | newFilter.push("all", existingFilter, mergeFilter) 1470 | } 1471 | 1472 | return newFilter; 1473 | } 1474 | 1475 | function openInJOSM() { 1476 | //Open map location in JOSM 1477 | var bounds = map.getBounds(); 1478 | var top = bounds.getNorth(); 1479 | var bottom = bounds.getSouth(); 1480 | var left = bounds.getWest(); 1481 | var right = bounds.getEast(); 1482 | // var josmUrl = 'https://127.0.0.1:8112/load_and_zoom?left='+left+'&right='+right+'&top='+top+'&bottom='+bottom; 1483 | var josmUrl = 'http://127.0.0.1:8111/load_and_zoom?left=' + left + '&right=' + right + '&top=' + top + '&bottom=' + bottom; 1484 | $.ajax(josmUrl, function() {}); 1485 | } 1486 | 1487 | function countProperty(geojson, property) { 1488 | var stats = {}; 1489 | geojson.features.forEach(function(feature) { 1490 | var val = feature.properties[property]; 1491 | stats[val] = stats[val] + 1 || 1; 1492 | }); 1493 | stats['total'] = geojson.features.length; 1494 | return stats; 1495 | } 1496 | 1497 | function getTurnRestrictions(callback) { 1498 | var bounds = map.getBounds(); 1499 | 1500 | var south = bounds.getSouth(), 1501 | west = bounds.getWest(), 1502 | north = bounds.getNorth(), 1503 | east = bounds.getEast(); 1504 | 1505 | var query = '[out:json][timeout:600];(relation[~"^restriction.*$"~"."](' + south + ',' + west + ',' + north + ',' + east + '););out body;>;out skel qt;' 1506 | 1507 | queryOverpass(query, function(error, data) { 1508 | if (error) callback(error); 1509 | data.features.forEach(function(feature) { 1510 | var props = feature.properties; 1511 | var keys = Object.keys(props); 1512 | keys.forEach(function(key) { 1513 | if (props[key] instanceof Array) { 1514 | props[key].forEach(function(prop) { 1515 | flatten(prop, key, props); 1516 | }); 1517 | } 1518 | 1519 | if (props[key] instanceof Object) { 1520 | flatten(props[key], key, props); 1521 | } 1522 | }); 1523 | }); 1524 | callback(null, data); 1525 | }, { 1526 | overpassUrl: 'https://overpass-cfn-production.tilestream.net/api/interpreter', 1527 | flatProperties: false 1528 | }) 1529 | } 1530 | 1531 | function flatten(obj, parentKey, properties) { 1532 | Object.keys(obj).forEach(function(key) { 1533 | if (obj[key] instanceof Object) { 1534 | flatten(obj[key], parentKey + '_' + key, properties); 1535 | delete properties[parentKey]; 1536 | } 1537 | properties[parentKey + '_' + key] = obj[key]; 1538 | delete properties[parentKey]; 1539 | }); 1540 | } 1541 | 1542 | function iconRotations() { 1543 | var stops = []; 1544 | for (var i = -360; i <= 360; i++) { 1545 | stops.push([i, i]); 1546 | } 1547 | return stops; 1548 | } 1549 | -------------------------------------------------------------------------------- /land.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "osm-navigation-map", 3 | "version": "0.0.1", 4 | "description": "A map style to explore and improve navigation data on OpenStreetMap", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "npm run watch & serve .", 8 | "watch": "watchify index.js -o build.js -v", 9 | "build": "browserify index.js -o build.js -v" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "osm-navigation-map" 14 | }, 15 | "keywords": [ 16 | "osm", 17 | "navigation", 18 | "map" 19 | ], 20 | "author": "Mapbox", 21 | "license": "ISC", 22 | "dependencies": { 23 | "hat": "0.0.3", 24 | "mapbox": "^1.0.0-beta3", 25 | "mapbox-data-team": "^1.1.2", 26 | "osm-auth": "^1.0.1", 27 | "query-overpass": "^1.1.0", 28 | "tile-cover": "^3.0.1", 29 | "turf": "^3.0.14" 30 | }, 31 | "devDependencies": { 32 | "browserify": "^13.1.0", 33 | "serve": "^1.4.0", 34 | "watchify": "^3.7.0" 35 | } 36 | } 37 | --------------------------------------------------------------------------------