transport.js
1 /*global $, L, URI*/ 2 3 var parsed; 4 var globalState = {}; 5 var map; 6 var sidebar; 7 var routeLayer; 8 var tileGroup; 9 10 L.LatLngBounds.prototype.trim = function (precision) { 11 this._northEast.lat = this._northEast.lat.toFixed(precision); 12 this._northEast.lng = this._northEast.lng.toFixed(precision); 13 this._southWest.lat = this._southWest.lat.toFixed(precision); 14 this._southWest.lng = this._southWest.lng.toFixed(precision); 15 return this; 16 }; 17 18 L.LatLngBounds.prototype.toXobbString = function () { 19 // Return bbox string compatible with Overpass API 20 return this._southWest.lat + "," + this._southWest.lng + "," + this._northEast.lat + "," + this._northEast.lng; 21 }; 22 23 var setTiles = function(tile) { 24 if (tile) { 25 localStorage.setItem("otv-tiles", tile); 26 } 27 tileGroup.clearLayers(); 28 tileGroup.addLayer(tiles[localStorage.getItem("otv-tiles")]); 29 } 30 31 var initMap = function() { 32 map = L.map('map').setView(defaultMapView.coords, defaultMapView.zoom); 33 tileGroup = L.layerGroup().addTo(map); 34 setTiles(); 35 36 sidebar = L.control.sidebar('sidebar').addTo(map); 37 38 // Ask user location. See map.on('locationfound') 39 map.locate(); 40 } 41 42 function updateURL() { 43 var uri = URI(); 44 uri.search(globalState); 45 history.pushState({globalState: globalState}, null, uri.toString()); 46 $("#dlForm input[type='text']").each(function () { 47 $(this).val(globalState[$(this).attr("name")]); 48 }); 49 } 50 51 function bindEvents() { 52 // To be executed on page load 53 54 var uri = URI(); 55 // Restore global state from URL parameters 56 globalState = uri.search(true); 57 if (globalState.bb) { 58 $("#bb-check").prop('checked', true); 59 } 60 61 $('#dlForm').submit(function (e) { 62 e.preventDefault(); 63 64 $(this).find("input[type='text']").each(function () { 65 if ($(this).val()) { 66 globalState[$(this).attr("name")] = $(this).val(); 67 } else { 68 delete globalState[$(this).attr("name")]; 69 } 70 }); 71 updateURL(); 72 getRouteMastersByParams(globalState.network, globalState.operator, globalState.ref, globalState.bb); 73 sidebar.open("data_display"); 74 }); 75 76 map.on('locationfound', function (l) { 77 map.fitBounds(l.bounds, mapPadding); 78 }); 79 80 map.on('moveend', function () { 81 globalState.lat = map.getCenter().lat.toFixed(5); 82 globalState.lng = map.getCenter().lng.toFixed(5); 83 globalState.z = map.getZoom(); 84 if ($("#bb-check").prop("checked")) { 85 globalState.bb = map.getBounds().trim(5).toXobbString(); 86 } 87 updateURL(); 88 }); 89 90 $(".otv-settings").on('change', function () { 91 localStorage.setItem($(this).attr('id'), $(this).val()); 92 }); 93 94 $("#bb-check").on('change', function () { 95 if ($(this).is(':checked')) { 96 globalState.bb = map.getBounds().trim(5).toXobbString(); 97 } else { 98 delete globalState.bb; 99 } 100 }); 101 102 $("#routemaster-tags-toggle").on("click", function () { 103 $("#routemaster-tags").toggle(); 104 }); 105 $("#route-tags-toggle").on("click", function () { 106 $("#route-tags").toggle(); 107 }); 108 109 $("#routemaster-displayAll").on("click", displayAllOnMap); 110 111 $("#routemaster-select") 112 .removeClass("hidden") 113 .change(function () { 114 globalState.selrm = $(this).val(); 115 $("#routemaster-select").val(globalState.selrm); 116 updateStatus("dl"); 117 updateURL(); 118 getRouteMasterById(globalState.selrm, 119 function (route_master) { 120 if (!route_master) { 121 updateStatus("fail", "No route_master found"); 122 } else { 123 updateStatus("ok"); 124 displayRoutes(route_master); 125 } 126 }, function () { 127 updateStatus("fail", "Error while getting route_master data"); 128 }); 129 }); 130 $("#otv-tiles").on("change", function() { 131 setTiles(); 132 }); 133 } 134 135 function updateStatus(status, msg) { 136 var level="info"; 137 138 $("li#data_tab i").removeClass().addClass("fa"); 139 switch (status) { 140 case "ok": 141 $("li#data_tab i").addClass("fa-bars"); 142 $("li#data_tab").removeClass("disabled"); 143 break; 144 case "dl": 145 $("li#data_tab i").addClass("fa-spin fa-spinner"); 146 $("li#data_tab").addClass("disabled"); 147 break; 148 case "fail": 149 level = "warning"; 150 $("li#data_tab").removeClass("disabled"); 151 $("li#data_tab i").addClass("fa-exclamation-triangle"); 152 break; 153 default: 154 break; 155 } 156 157 var divMessage = ""; 158 if (msg) { 159 divMessage = msg; 160 } else if (defaultStatusMessages[status] !== undefined) { 161 divMessage = defaultStatusMessages[status]; 162 } 163 if (divMessage.length > 0) { 164 $("#data-status") 165 .removeClass() 166 .addClass(level) 167 .text(divMessage); 168 } else { 169 $("#data-status").empty(); 170 } 171 } 172 173 function guessQuery() { 174 // Find what to do on page opening 175 if (globalState.rmid) { 176 getRouteMasterById(globalState.rmid); 177 sidebar.open("data_display"); 178 } else if (globalState.rmid || globalState.network || globalState.operator || globalState.bb) { // Avoid queries which can match too much routes 179 getRouteMastersByParams(globalState.network, globalState.operator, globalState.ref, globalState.bb); 180 sidebar.open("data_display"); 181 } else if (! localStorage.getItem("otv-readIntro")){ 182 // User has not yet read the help, and haven't made a request or been linked to one 183 localStorage.setItem("otv-readIntro", true); 184 sidebar.open("info-tab"); 185 } else { 186 sidebar.open("query"); 187 } 188 updateURL(); 189 } 190 191 function displayRouteMasters() { 192 if (!Object.keys(parsed.route_masters).length) { 193 updateStatus("fail", "No route_masters found"); 194 return; 195 } 196 updateStatus("ok"); 197 $("#routemaster-displayAll").removeClass("hidden"); 198 var sorted = _.sortBy(parsed.route_masters, function (e) {return e.tags.name;}); 199 $("#routemaster-select").empty(); 200 $.each(sorted, function (i, r) { 201 $("#routemaster-select").append($('<option>', { 202 value: r.id, 203 text: r.tags.name || r.id, 204 })); 205 }); 206 // Always trigger route variant display 207 if(globalState.selrm) { 208 $("#routemaster-select").val(globalState.selrm); 209 } 210 $("#routemaster-select").change(); 211 } 212 213 function getRouteMastersByParams(network, operator, ref, bbox) { 214 updateStatus("dl"); 215 216 var netstr = network ? ("[network~'" + network + "',i]") : ""; 217 var opstr = operator ? ("[operator~'" + operator + "',i]") : ""; 218 var refstr = ref ? ("[ref~'^" + ref + "$',i]") : ""; 219 220 var base; 221 if (bbox) { 222 // Bounding-box filter only works with first-level members of relation 223 // It doesn't recurse into member relations 224 bbox = bbox ? ("(" + bbox + ")") : ""; 225 base = 'relation["type"="route"]' + bbox + ";" + 226 'relation(br)' + netstr + opstr + refstr; 227 } else { 228 base = 'relation["type"="route_master"]' + netstr + opstr + refstr; 229 } 230 getRouteMastersData(base, displayRouteMasters); 231 } 232 233 function clearMap() { 234 if (map.hasLayer(routeLayer)) { 235 map.removeLayer(routeLayer); 236 } 237 routeLayer = L.layerGroup(); 238 } 239 240 function displayOnMap(route) { 241 _.each(route.stop_positions, function (obj) { 242 makeMarker(obj, routeLayer); 243 }); 244 _.each(route.platforms, function (obj) { 245 makeMarker(obj, routeLayer); 246 }); 247 _.each(route.paths, function (obj) { 248 makeMarker(obj, routeLayer, { 249 color: route.tags.colour || path_color[route.tags.route] || "red", 250 251 }); 252 }); 253 if(routeLayer.getLayers().length > 0) { 254 if(window.innerWidth > mapPadding["paddingTopLeft"][0]) { 255 map.fitBounds(L.featureGroup(routeLayer.getLayers()).getBounds(), mapPadding); 256 } else { 257 map.fitBounds(L.featureGroup(routeLayer.getLayers()).getBounds()); 258 } 259 routeLayer.addTo(map); 260 } 261 } 262 263 function getTagTable(obj) { 264 var tagStr = "<table class='tags'>"; 265 Object.keys(obj.tags).forEach(function (key) { 266 tagStr += `<tr><td class='key'>${key}</td><td class='value'>${obj.tags[key].autoLink()}</td></tr>`; 267 }); 268 tagStr += "</table>"; 269 return tagStr; 270 } 271 272 function getLatLngArray(osmWay) { 273 var latlngs = []; 274 _.each(osmWay.nodes, function (n) { 275 latlngs.push(L.latLng(n.lat, n.lon)); 276 }); 277 return latlngs; 278 } 279 280 function makeMarker(obj, group, overrideStyle) { 281 var markerOptions = { 282 autoPan: false 283 }; 284 var popupHTML = `<a href='${osmUrl}${obj.type}/${obj.id}'><h1>${obj.tags.name || "!Missing name!"}</h1></a> 285 ${getTagTable(obj)}`; 286 if (obj.type == "way") { 287 var latlngs = getLatLngArray(obj); 288 if (obj.tags.public_transport === "platform") { 289 obj.layer = L.polyline(latlngs,{ 290 color: 'blue', 291 weight: 8 292 }).bindPopup(popupHTML, markerOptions); 293 } 294 else { 295 obj.layer = L.polyline(latlngs,$.extend({ 296 weight: 4 297 }, overrideStyle)).bindPopup(popupHTML, markerOptions); 298 } 299 } else { 300 if (obj.tags.public_transport === "stop_position") { 301 obj.layer = L.marker([obj.lat, obj.lon], { 302 icon: iconStopPosition 303 }).bindPopup(popupHTML, markerOptions); 304 } else if (obj.tags.public_transport === "platform") { 305 obj.layer = L.marker([obj.lat, obj.lon], { 306 icon: platformIcon 307 }).bindPopup(popupHTML, markerOptions); 308 } else { 309 obj.layer = L.marker([obj.lat, obj.lon]) 310 .bindPopup(popupHTML, markerOptions); 311 } 312 } 313 group.addLayer(obj.layer); 314 } 315 316 function displayRoutes(route_master) { 317 //Display informations relative to the route_master chosen 318 319 $("#routemaster-tags-toggle").removeClass("hidden"); 320 $("#routemaster-name").text(route_master.tags.name); 321 $("#routemaster-tags").html(getTagTable(route_master)); 322 323 // Clear data display before displaying new route variants 324 $("#routes_list ul").empty(); 325 $('#stops-list').find("li").remove(); 326 327 _.each(route_master.members, function (r) { 328 var routeLi = $("<li>").addClass(r.tags.route + "_route"); 329 330 $("<a>", {href: osmUrl + "relation" + "/" + r.id}) 331 .attr("target","_blank") 332 .append($("<img>", {src: route_icons[r.tags.route], alt: "route on osm.org"})) 333 .appendTo(routeLi); 334 335 $("<span>") 336 .text(r.tags.name) 337 .prop("title", r.tags.name) 338 .attr("data-osmid", r.id) 339 .on("click", function () { 340 $("#routes_list>ul>li>span").removeClass("selected_route"); 341 $(this).addClass("selected_route"); 342 updateURL(); 343 displayRouteData(parsed.routes[$(this).data("osmid")]); 344 clearMap(); 345 displayOnMap(parsed.routes[$(this).data("osmid")]); 346 }) 347 .appendTo(routeLi); 348 349 $("#routes_list>ul").append(routeLi); 350 }); 351 // Display the first route variant if it exists 352 if(route_master.members[0]) { 353 var id = route_master.members[0].id; 354 $(`span[data-osmID=${id}]`).addClass("selected_route"); 355 clearMap(); 356 displayRouteData(parsed.routes[id]); 357 displayOnMap(parsed.routes[id]); 358 } 359 } 360 361 function displayAllOnMap() { 362 clearMap(); 363 _.each(parsed.routes, displayOnMap); 364 } 365 366 function displayRouteData(route) { 367 // Display route tags 368 $("#route-tags").html(getTagTable(route)); 369 $("#route-tags-toggle").show(); 370 371 // Clear data display before new display 372 $('#stops-list').find("li").remove(); 373 var master_li; 374 _.each(route.members, function (member) { 375 if (!member.role.match(/stop(_entry_only|_exit_only)?/)) { 376 return; 377 } 378 master_li = $("<li>"); 379 stop_ul = $("<ul>"); 380 if (member.stop_area) { 381 $("<a>", {href: osmUrl + "relation/" + member.stop_area.id}) 382 .append($("<img>", {src: "img/relation.svg", alt: "stop_area relation"})) 383 .appendTo(master_li); 384 $("<span>").html(member.stop_area.tags.name || member.stop_area.id) 385 .addClass("route_master-name") 386 .appendTo(master_li); 387 } else { 388 $("<span>") 389 .text("Missing stop_area relation") 390 .addClass("route_master-name") 391 .appendTo(master_li); 392 } 393 stop_li = $("<li>"); 394 $("<a>", {href: osmUrl + member.type + "/" + member.id, "data-osm": member.id}) 395 .append($("<img>", {src: "img/stop_position_32.png", alt: "Stop_position"})) 396 .appendTo(stop_li); 397 $("<span>") 398 .text("♿") 399 .addClass("wheelchair feature_" + member.tags.wheelchair) 400 .appendTo(stop_li); 401 $("<span>").html(member.tags.name || member.id) 402 .appendTo(stop_li); 403 stop_li.on("click", null, member, function () { 404 member.layer.openPopup(); 405 }) 406 .on("mouseleave", null, member, function () { 407 member.layer.closePopup(); 408 }); 409 stop_ul.append(stop_li); 410 411 var platforms = findPlatforms(route, member.stop_area); 412 _.each(platforms, function (platform) { 413 var platform_li = $("<li>"); 414 $("<a>", {href: osmUrl + platform.type + "/" + platform.id}) 415 .append($("<img>", {src: "img/platform_14.png", alt: "Platform"})) 416 .appendTo(platform_li); 417 $("<span>") 418 .text("♿") 419 .addClass("wheelchair feature_" + platform.tags.wheelchair) 420 .appendTo(platform_li); 421 $("<span>") 422 .text(platform.tags.name || platform.id) 423 .appendTo(platform_li); 424 425 platform_li.on("click", null, member, function () { 426 platform.layer.openPopup(); 427 }) 428 .on("mouseleave", null, platform, function () { 429 platform.layer.closePopup(); 430 }) 431 .appendTo(stop_ul); 432 }); 433 master_li.append(stop_ul); 434 $('#stops-list').append(master_li); 435 }); 436 } 437 438 function findPlatforms(route, stop_area) { 439 if (!stop_area) { 440 return []; 441 } 442 platform_regex = new RegExp("platform(_entry_only|_exit_only)?"); 443 var route_platforms = _.filter(route.members, function (p){return platform_regex.test(p.role);}); 444 var area_platforms = _.filter(stop_area.members, function (p){return platform_regex.test(p.role);}); 445 return _.intersection(route_platforms, area_platforms); 446 } 447 448 function initOptions() { 449 for (var o in defaultOptions) { 450 if (defaultOptions.hasOwnProperty(o) && !localStorage.getItem(o)) { 451 localStorage.setItem(o, defaultOptions[o]); 452 } 453 } 454 $(".otv-settings").each(function () { 455 $(this).val(localStorage.getItem($(this).attr('id'))); 456 }); 457 } 458 459 initOptions(); 460 initMap(); 461 bindEvents(); 462 guessQuery();