├── assets ├── JungleBus_TransportHours.js ├── osmtogeojson.js └── papaparse.min.js ├── commons.js ├── favicon.ico ├── i18n ├── ca.json ├── cs.json ├── de.json ├── en.json ├── es.json ├── fr.json ├── hu.json ├── it.json ├── pl.json ├── pt.json └── readme.md ├── img ├── Logo_Unroll.png ├── background.png ├── osm.svg └── stop.png ├── index.html ├── index.js ├── line_data.js ├── load.html ├── load.js ├── main.css ├── print.css ├── readme.md ├── route.html ├── route.js ├── screenshots ├── abidjan.png ├── index.png └── paris.png └── taginfo.json /assets/JungleBus_TransportHours.js: -------------------------------------------------------------------------------- 1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.TransportHours = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i 0) { 91 | throw new Error("The given periods are not valid"); 92 | } 93 | 94 | options = Object.assign({ 95 | explicitPH: false 96 | }, options); 97 | var ohHours = periods.map(function (p) { 98 | return { 99 | days: p.days, 100 | hours: OpeningHoursBuilder.HoursToOH(p.hours) 101 | }; 102 | }); 103 | var ohHoursDays = {}; 104 | ohHours.forEach(function (p) { 105 | if (ohHoursDays[p.hours]) { 106 | ohHoursDays[p.hours] = ohHoursDays[p.hours].concat(p.days); 107 | } else { 108 | ohHoursDays[p.hours] = p.days; 109 | } 110 | }); 111 | var ohPeriods = Object.entries(ohHoursDays).map(function (e) { 112 | return [OpeningHoursBuilder.DaysToOH(e[1]), e[0]]; 113 | }); 114 | this._ohValue = ohPeriods.map(function (p) { 115 | return p.join(" ").trim(); 116 | }).join("; "); 117 | 118 | if (!options.explicitPH && (this._ohValue === "00:00-24:00" || this._ohValue === "Mo-Su,PH 00:00-24:00")) { 119 | this._ohValue = "24/7"; 120 | } 121 | 122 | var distinctDays = _toConsumableArray(new Set(periods.map(function (p) { 123 | return p.days; 124 | }).flat())); 125 | 126 | if (options.explicitPH && distinctDays.length === 7 && !distinctDays.includes("ph")) { 127 | this._ohValue += "; PH off"; 128 | } 129 | } 130 | 131 | _createClass(OpeningHoursBuilder, [{ 132 | key: "getValue", 133 | value: function getValue() { 134 | return this._ohValue; 135 | } 136 | }], [{ 137 | key: "IsPeriodValid", 138 | value: function IsPeriodValid(p) { 139 | return p && p.days && p.days.length > 0 && p.days.filter(function (d) { 140 | return !d || typeof d !== "string" || !DAYS.includes(d); 141 | }).length === 0 && p.hours && p.hours.length > 0 && p.hours.filter(function (h) { 142 | return !h || typeof h !== "string" || !HOURS_RGX.test(h); 143 | }).length === 0; 144 | } 145 | }, { 146 | key: "DaysToOH", 147 | value: function DaysToOH(days) { 148 | var daysId = _toConsumableArray(new Set(days.map(function (d) { 149 | return DAYS.indexOf(d); 150 | }))).sort(); 151 | 152 | for (var id = 1; id < daysId.length; id++) { 153 | var currDay = daysId[id]; 154 | var prevDay = daysId[id - 1]; 155 | 156 | if (Array.isArray(prevDay)) { 157 | prevDay = prevDay[1]; 158 | } 159 | 160 | if (currDay === prevDay + 1 && currDay !== DAYS.indexOf("ph")) { 161 | if (Array.isArray(daysId[id - 1])) { 162 | daysId[id - 1][1] = currDay; 163 | } else { 164 | daysId[id - 1] = [prevDay, currDay]; 165 | } 166 | 167 | daysId.splice(id, 1); 168 | id--; 169 | } 170 | } 171 | 172 | var dayPart = daysId.map(function (dId) { 173 | return Array.isArray(dId) ? dId.map(function (d) { 174 | return DAYS_OH[d]; 175 | }).join(dId[1] - dId[0] > 1 ? "-" : ",") : DAYS_OH[dId]; 176 | }).join(","); 177 | return dayPart; 178 | } 179 | }, { 180 | key: "HoursToOH", 181 | value: function HoursToOH(hours) { 182 | var minutesRanges = hours.map(function (h) { 183 | return h.split("-").map(function (hp) { 184 | return OpeningHoursBuilder.TimeToMinutes(hp); 185 | }); 186 | }).sort(function (a, b) { 187 | return a[0] - b[0]; 188 | }); 189 | 190 | for (var id = 1; id < minutesRanges.length; id++) { 191 | var currRange = minutesRanges[id]; 192 | var prevRange = minutesRanges[id - 1]; 193 | 194 | if (prevRange[1] >= currRange[0]) { 195 | if (prevRange[1] < currRange[1] || currRange[0] > currRange[1]) { 196 | prevRange[1] = currRange[1]; 197 | } 198 | 199 | minutesRanges.splice(id, 1); 200 | id--; 201 | } 202 | } 203 | 204 | var hourPart = minutesRanges.map(function (mr) { 205 | return mr.map(function (mrp) { 206 | return OpeningHoursBuilder.MinutesToTime(mrp); 207 | }).join("-"); 208 | }).join(","); 209 | return hourPart; 210 | } 211 | }, { 212 | key: "TimeToMinutes", 213 | value: function TimeToMinutes(time) { 214 | var parts = time.split(":").map(function (p) { 215 | return parseInt(p); 216 | }); 217 | return parts[0] * 60 + parts[1]; 218 | } 219 | }, { 220 | key: "MinutesToTime", 221 | value: function MinutesToTime(minutes) { 222 | var twoDigits = function twoDigits(v) { 223 | return v < 10 ? "0" + v.toString() : v.toString(); 224 | }; 225 | 226 | return twoDigits(Math.floor(minutes / 60).toFixed(0)) + ":" + twoDigits(minutes % 60); 227 | } 228 | }]); 229 | 230 | return OpeningHoursBuilder; 231 | }(); 232 | 233 | module.exports = OpeningHoursBuilder; 234 | 235 | },{"array-flat-polyfill":1}],4:[function(require,module,exports){ 236 | "use strict"; 237 | 238 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 239 | 240 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } 241 | 242 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } 243 | 244 | var OpeningHoursParser = function () { 245 | function OpeningHoursParser(value) { 246 | _classCallCheck(this, OpeningHoursParser); 247 | 248 | this.openingHours = {}; 249 | 250 | this._parse(value); 251 | 252 | if (Object.values(this.openingHours).filter(function (oh) { 253 | return oh.length === 0; 254 | }).length === Object.keys(this.openingHours).length) { 255 | throw new Error("Can't parse opening_hours : " + value); 256 | } 257 | } 258 | 259 | _createClass(OpeningHoursParser, [{ 260 | key: "getTable", 261 | value: function getTable() { 262 | return this.openingHours; 263 | } 264 | }, { 265 | key: "_parse", 266 | value: function _parse(inp) { 267 | var _this = this; 268 | 269 | this._initOpeningHoursObj(); 270 | 271 | inp = this._simplify(inp); 272 | 273 | var parts = this._splitHard(inp); 274 | 275 | parts.forEach(function (part) { 276 | _this._parseHardPart(part); 277 | }); 278 | } 279 | }, { 280 | key: "_simplify", 281 | value: function _simplify(input) { 282 | if (input == "24/7") { 283 | input = "mo-su 00:00-24:00; ph 00:00-24:00"; 284 | } 285 | 286 | input = input.toLocaleLowerCase(); 287 | input = input.trim(); 288 | input = input.replace(/ +(?= )/g, ''); 289 | input = input.replace(' -', '-'); 290 | input = input.replace('- ', '-'); 291 | input = input.replace(' :', ':'); 292 | input = input.replace(': ', ':'); 293 | input = input.replace(' ,', ','); 294 | input = input.replace(', ', ','); 295 | input = input.replace(' ;', ';'); 296 | input = input.replace('; ', ';'); 297 | return input; 298 | } 299 | }, { 300 | key: "_splitHard", 301 | value: function _splitHard(inp) { 302 | return inp.split(';'); 303 | } 304 | }, { 305 | key: "_parseHardPart", 306 | value: function _parseHardPart(part) { 307 | var _this2 = this; 308 | 309 | if (part == "24/7") { 310 | part = "mo-su 00:00-24:00"; 311 | } 312 | 313 | var segments = part.split(/\ |\,/); 314 | var tempData = {}; 315 | var days = []; 316 | var times = []; 317 | segments.forEach(function (segment, i) { 318 | if (_this2._checkDay(segment)) { 319 | if (times.length === 0) { 320 | days = days.concat(_this2._parseDays(segment)); 321 | } else { 322 | days.forEach(function (day) { 323 | if (tempData[day]) { 324 | tempData[day] = tempData[day].concat(times); 325 | } else { 326 | tempData[day] = times; 327 | } 328 | }); 329 | days = _this2._parseDays(segment); 330 | times = []; 331 | } 332 | } 333 | 334 | if (_this2._checkTime(segment)) { 335 | if (i === 0 && days.length === 0) { 336 | days = _this2._parseDays("Mo-Su,PH"); 337 | } 338 | 339 | if (segment == "off") { 340 | times = "off"; 341 | } else { 342 | times.push(_this2._cleanTime(segment)); 343 | } 344 | } 345 | }); 346 | days.forEach(function (day) { 347 | if (tempData[day]) { 348 | tempData[day] = tempData[day].concat(times); 349 | } else { 350 | tempData[day] = times; 351 | } 352 | }); 353 | days.forEach(function (day) { 354 | if (times === "off") { 355 | tempData[day] = []; 356 | } else if (times.length === 0) { 357 | tempData[day] = ["00:00-24:00"]; 358 | } 359 | }); 360 | 361 | for (var key in tempData) { 362 | this.openingHours[key] = tempData[key]; 363 | } 364 | } 365 | }, { 366 | key: "_parseDays", 367 | value: function _parseDays(part) { 368 | var _this3 = this; 369 | 370 | part = part.toLowerCase(); 371 | var days = []; 372 | var softparts = part.split(','); 373 | softparts.forEach(function (part) { 374 | var rangecount = (part.match(/\-/g) || []).length; 375 | 376 | if (rangecount == 0) { 377 | days.push(part); 378 | } else { 379 | days = days.concat(_this3._calcDayRange(part)); 380 | } 381 | }); 382 | return days; 383 | } 384 | }, { 385 | key: "_cleanTime", 386 | value: function _cleanTime(time) { 387 | if (time.match(/^[0-9]:[0-9]{2}/)) { 388 | time = "0" + time; 389 | } 390 | 391 | if (time.match(/^[0-9]{2}:[0-9]{2}\-[0-9]:[0-9]{2}/)) { 392 | time = time.substring(0, 6) + "0" + time.substring(6); 393 | } 394 | 395 | return time; 396 | } 397 | }, { 398 | key: "_initOpeningHoursObj", 399 | value: function _initOpeningHoursObj() { 400 | this.openingHours = { 401 | su: [], 402 | mo: [], 403 | tu: [], 404 | we: [], 405 | th: [], 406 | fr: [], 407 | sa: [], 408 | ph: [] 409 | }; 410 | } 411 | }, { 412 | key: "_calcDayRange", 413 | value: function _calcDayRange(range) { 414 | var def = { 415 | su: 0, 416 | mo: 1, 417 | tu: 2, 418 | we: 3, 419 | th: 4, 420 | fr: 5, 421 | sa: 6 422 | }; 423 | var rangeElements = range.split('-'); 424 | var dayStart = def[rangeElements[0]]; 425 | var dayEnd = def[rangeElements[1]]; 426 | 427 | var numberRange = this._calcRange(dayStart, dayEnd, 6); 428 | 429 | var outRange = []; 430 | numberRange.forEach(function (n) { 431 | for (var key in def) { 432 | if (def[key] == n) { 433 | outRange.push(key); 434 | } 435 | } 436 | }); 437 | return outRange; 438 | } 439 | }, { 440 | key: "_calcRange", 441 | value: function _calcRange(min, max, maxval) { 442 | if (min == max) { 443 | return [min]; 444 | } 445 | 446 | var range = [min]; 447 | var rangepoint = min; 448 | 449 | while (rangepoint < (min < max ? max : maxval)) { 450 | rangepoint++; 451 | range.push(rangepoint); 452 | } 453 | 454 | if (min > max) { 455 | range = range.concat(this._calcRange(0, max, maxval)); 456 | } 457 | 458 | return range; 459 | } 460 | }, { 461 | key: "_checkTime", 462 | value: function _checkTime(inp) { 463 | if (inp.match(/[0-9]{1,2}:[0-9]{2}\+/)) { 464 | return true; 465 | } 466 | 467 | if (inp.match(/[0-9]{1,2}:[0-9]{2}\-[0-9]{1,2}:[0-9]{2}/)) { 468 | return true; 469 | } 470 | 471 | if (inp.match(/off/)) { 472 | return true; 473 | } 474 | 475 | return false; 476 | } 477 | }, { 478 | key: "_checkDay", 479 | value: function _checkDay(inp) { 480 | var days = ["mo", "tu", "we", "th", "fr", "sa", "su", "ph"]; 481 | 482 | if (inp.match(/\-/g)) { 483 | var rangelements = inp.split('-'); 484 | 485 | if (days.indexOf(rangelements[0]) !== -1 && days.indexOf(rangelements[1]) !== -1) { 486 | return true; 487 | } 488 | } else { 489 | if (days.indexOf(inp) !== -1) { 490 | return true; 491 | } 492 | } 493 | 494 | return false; 495 | } 496 | }]); 497 | 498 | return OpeningHoursParser; 499 | }(); 500 | 501 | module.exports = OpeningHoursParser; 502 | 503 | },{}],5:[function(require,module,exports){ 504 | "use strict"; 505 | 506 | function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); } 507 | 508 | function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance"); } 509 | 510 | function _iterableToArray(iter) { if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); } 511 | 512 | function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } } 513 | 514 | function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); } 515 | 516 | function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } 517 | 518 | function _iterableToArrayLimit(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } 519 | 520 | function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } 521 | 522 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 523 | 524 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } 525 | 526 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } 527 | 528 | require("array-flat-polyfill"); 529 | 530 | var OpeningHoursParser = require("./OpeningHoursParser"); 531 | 532 | var OpeningHoursBuilder = require("./OpeningHoursBuilder"); 533 | 534 | var deepEqual = require("fast-deep-equal"); 535 | 536 | var TAG_UNSET = "unset"; 537 | var TAG_INVALID = "invalid"; 538 | var DAYS_ID = ["mo", "tu", "we", "th", "fr", "sa", "su", "ph"]; 539 | 540 | var TransportHours = function () { 541 | function TransportHours() { 542 | _classCallCheck(this, TransportHours); 543 | } 544 | 545 | _createClass(TransportHours, [{ 546 | key: "tagsToHoursObject", 547 | value: function tagsToHoursObject(tags) { 548 | var opens; 549 | 550 | try { 551 | opens = tags.opening_hours ? new OpeningHoursParser(tags.opening_hours).getTable() : TAG_UNSET; 552 | } catch (e) { 553 | opens = TAG_INVALID; 554 | } 555 | 556 | var interval; 557 | 558 | try { 559 | interval = tags.interval ? this.intervalStringToMinutes(tags.interval) : TAG_UNSET; 560 | } catch (e) { 561 | interval = TAG_INVALID; 562 | } 563 | 564 | var intervalCond, intervalCondByDay; 565 | 566 | try { 567 | intervalCond = tags["interval:conditional"] ? this.intervalConditionalStringToObject(tags["interval:conditional"]) : TAG_UNSET; 568 | intervalCondByDay = intervalCond !== TAG_UNSET ? this._intervalConditionObjectToIntervalByDays(intervalCond) : TAG_UNSET; 569 | } catch (e) { 570 | intervalCond = TAG_INVALID; 571 | intervalCondByDay = TAG_INVALID; 572 | } 573 | 574 | var computedIntervals; 575 | 576 | try { 577 | computedIntervals = this._computeAllIntervals(opens, interval, intervalCondByDay); 578 | } catch (e) { 579 | computedIntervals = TAG_INVALID; 580 | } 581 | 582 | return { 583 | opens: opens, 584 | defaultInterval: interval, 585 | otherIntervals: intervalCond, 586 | otherIntervalsByDays: intervalCondByDay, 587 | allComputedIntervals: computedIntervals 588 | }; 589 | } 590 | }, { 591 | key: "intervalsObjectToTags", 592 | value: function intervalsObjectToTags(allIntervals) { 593 | var _this = this; 594 | 595 | var result = {}; 596 | var periodsOH = allIntervals.map(function (p) { 597 | return { 598 | days: p.days, 599 | hours: Object.keys(p.intervals) 600 | }; 601 | }); 602 | result.opening_hours = new OpeningHoursBuilder(periodsOH, { 603 | explicitPH: true 604 | }).getValue(); 605 | var intervalDuration = {}; 606 | allIntervals.forEach(function (p) { 607 | var nbDays = p.days.length; 608 | Object.entries(p.intervals).forEach(function (e) { 609 | var _e2 = _slicedToArray(e, 2), 610 | timerange = _e2[0], 611 | interval = _e2[1]; 612 | 613 | var duration = _this._timerangeDuration(timerange); 614 | 615 | if (intervalDuration[interval]) { 616 | intervalDuration[interval] += duration * nbDays; 617 | } else { 618 | intervalDuration[interval] = duration * nbDays; 619 | } 620 | }); 621 | }); 622 | var nbIntervals = Object.keys(intervalDuration).length; 623 | 624 | if (nbIntervals === 0) { 625 | throw new Error("No interval is defined in given periods"); 626 | } else if (nbIntervals === 1) { 627 | result.interval = Object.keys(intervalDuration)[0].toString(); 628 | } else { 629 | var defaultInterval, maxDuration; 630 | Object.entries(intervalDuration).forEach(function (e) { 631 | if (!defaultInterval || e[1] > maxDuration) { 632 | defaultInterval = e[0]; 633 | maxDuration = e[1]; 634 | } 635 | }); 636 | result.interval = defaultInterval.toString(); 637 | var intervalPeriods = []; 638 | Object.entries(intervalDuration).sort(function (a, b) { 639 | return b[1] - a[1]; 640 | }).filter(function (e) { 641 | return e[0] !== defaultInterval; 642 | }).forEach(function (e) { 643 | var interval = parseInt(e[0]); 644 | var applies = allIntervals.map(function (p) { 645 | var filtered = { 646 | days: p.days 647 | }; 648 | filtered.hours = Object.entries(p.intervals).filter(function (pe) { 649 | return pe[1] === interval; 650 | }).map(function (pe) { 651 | return pe[0]; 652 | }); 653 | return filtered; 654 | }).filter(function (p) { 655 | return p.hours.length > 0; 656 | }); 657 | 658 | if (applies.length > 0) { 659 | intervalPeriods.push({ 660 | interval: interval, 661 | applies: applies 662 | }); 663 | } 664 | }); 665 | result["interval:conditional"] = intervalPeriods.map(function (ip) { 666 | return ip.interval + " @ (" + new OpeningHoursBuilder(ip.applies).getValue() + ")"; 667 | }).join("; "); 668 | } 669 | 670 | return result; 671 | } 672 | }, { 673 | key: "_timerangeDuration", 674 | value: function _timerangeDuration(timerange) { 675 | var _timerange$split$map = timerange.split("-").map(function (t) { 676 | return OpeningHoursBuilder.TimeToMinutes(t); 677 | }), 678 | _timerange$split$map2 = _slicedToArray(_timerange$split$map, 2), 679 | startMin = _timerange$split$map2[0], 680 | endMin = _timerange$split$map2[1]; 681 | 682 | return startMin <= endMin ? endMin - startMin : 24 * 60 - startMin + endMin; 683 | } 684 | }, { 685 | key: "_computeAllIntervals", 686 | value: function _computeAllIntervals(openingHours, interval, intervalCondByDay) { 687 | var _this2 = this; 688 | 689 | if (openingHours === TAG_INVALID || interval === TAG_INVALID || interval === TAG_UNSET || intervalCondByDay === TAG_INVALID) { 690 | return (openingHours === TAG_INVALID || interval === TAG_INVALID) && intervalCondByDay === TAG_UNSET ? TAG_INVALID : intervalCondByDay; 691 | } else { 692 | var myIntervalCondByDay = intervalCondByDay === TAG_UNSET ? [] : intervalCondByDay; 693 | var myOH = openingHours; 694 | 695 | if (openingHours === TAG_UNSET) { 696 | myOH = new OpeningHoursParser("24/7").getTable(); 697 | } 698 | 699 | var result = []; 700 | myIntervalCondByDay.forEach(function (di) { 701 | di.days.forEach(function (d) { 702 | result.push({ 703 | days: [d], 704 | intervals: di.intervals 705 | }); 706 | }); 707 | }); 708 | result = result.map(function (di) { 709 | var ohDay = myOH[di.days[0]]; 710 | di.intervals = _this2._mergeIntervalsSingleDay(ohDay, interval, di.intervals); 711 | return di; 712 | }); 713 | 714 | var daysInCondInt = _toConsumableArray(new Set(myIntervalCondByDay.map(function (d) { 715 | return d.days; 716 | }).flat())); 717 | 718 | var missingDays = Object.keys(myOH).filter(function (d) { 719 | return !daysInCondInt.includes(d); 720 | }); 721 | var missingDaysOH = {}; 722 | missingDays.forEach(function (day) { 723 | missingDaysOH[day] = myOH[day]; 724 | }); 725 | result = result.concat(this._intervalConditionObjectToIntervalByDays([{ 726 | interval: interval, 727 | applies: missingDaysOH 728 | }])); 729 | 730 | for (var i = 1; i < result.length; i++) { 731 | for (var j = 0; j < i; j++) { 732 | if (deepEqual(result[i].intervals, result[j].intervals)) { 733 | result[j].days = result[j].days.concat(result[i].days); 734 | result.splice(i, 1); 735 | i--; 736 | break; 737 | } 738 | } 739 | } 740 | 741 | result.forEach(function (r) { 742 | return r.days.sort(function (a, b) { 743 | return DAYS_ID.indexOf(a) - DAYS_ID.indexOf(b); 744 | }); 745 | }); 746 | result.sort(function (a, b) { 747 | return DAYS_ID.indexOf(a.days[0]) - DAYS_ID.indexOf(b.days[0]); 748 | }); 749 | return result; 750 | } 751 | } 752 | }, { 753 | key: "_hourRangeWithin", 754 | value: function _hourRangeWithin(wider, smaller) { 755 | if (deepEqual(wider, smaller)) { 756 | return true; 757 | } else { 758 | if (wider[0] <= wider[1]) { 759 | if (smaller[0] > smaller[1]) { 760 | return false; 761 | } else { 762 | return wider[0] <= smaller[0] && smaller[0] < wider[1] && wider[0] < smaller[1] && smaller[1] <= wider[1]; 763 | } 764 | } else { 765 | if (smaller[0] <= smaller[1]) { 766 | if (wider[0] <= smaller[0] && wider[0] <= smaller[1]) { 767 | return true; 768 | } else { 769 | return false; 770 | } 771 | } else { 772 | return wider[0] <= smaller[0] && smaller[0] <= "24:00" && "00:00" <= smaller[1] && smaller[1] <= wider[1]; 773 | } 774 | } 775 | } 776 | } 777 | }, { 778 | key: "_mergeIntervalsSingleDay", 779 | value: function _mergeIntervalsSingleDay(hours, interval, condIntervals) { 780 | var _this3 = this; 781 | 782 | var hourRangeToArr = function hourRangeToArr(hr) { 783 | return hr.map(function (h) { 784 | return h.split("-"); 785 | }); 786 | }; 787 | 788 | var ohHours = hourRangeToArr(hours); 789 | var condHours = hourRangeToArr(Object.keys(condIntervals)); 790 | var invalidCondHours = condHours.filter(function (ch) { 791 | var foundOhHours = false; 792 | 793 | for (var i = 0; i < ohHours.length; i++) { 794 | var ohh = ohHours[i]; 795 | 796 | if (_this3._hourRangeWithin(ohh, ch)) { 797 | foundOhHours = true; 798 | break; 799 | } 800 | } 801 | 802 | return !foundOhHours; 803 | }); 804 | 805 | if (invalidCondHours.length > 0) { 806 | throw new Error("Conditional intervals are not contained in opening hours"); 807 | } 808 | 809 | var goneOverMidnight = false; 810 | condHours.sort(function (a, b) { 811 | return _this3.intervalStringToMinutes(a[0]) - _this3.intervalStringToMinutes(b[0]); 812 | }); 813 | var overlappingCondHours = condHours.filter(function (ch, i) { 814 | if (!goneOverMidnight) { 815 | if (ch[0] > ch[1]) { 816 | goneOverMidnight = true; 817 | } 818 | 819 | return i > 0 ? condHours[i - 1][1] > ch[0] : false; 820 | } else { 821 | return true; 822 | } 823 | }); 824 | 825 | if (overlappingCondHours.length > 0) { 826 | throw new Error("Conditional intervals are not exclusive (they overlaps)"); 827 | } 828 | 829 | var ohHoursWithoutConds = []; 830 | ohHours.forEach(function (ohh, i) { 831 | var holes = []; 832 | var thisCondHours = condHours.filter(function (ch) { 833 | return _this3._hourRangeWithin(ohh, ch); 834 | }); 835 | thisCondHours.forEach(function (ch, i) { 836 | var isFirst = i === 0; 837 | var isLast = i === thisCondHours.length - 1; 838 | 839 | if (isFirst && ohh[0] < ch[0]) { 840 | holes.push(ohh[0]); 841 | holes.push(ch[0]); 842 | } 843 | 844 | if (!isFirst && thisCondHours[i - 1][1] < ch[0]) { 845 | holes.push(thisCondHours[i - 1][1]); 846 | holes.push(ch[0]); 847 | } 848 | 849 | if (isLast && ch[1] < ohh[1]) { 850 | holes.push(ch[1]); 851 | holes.push(ohh[1]); 852 | } 853 | }); 854 | ohHoursWithoutConds = ohHoursWithoutConds.concat(holes.map(function (h, i) { 855 | return i % 2 === 0 ? null : holes[i - 1] + "-" + h; 856 | }).filter(function (h) { 857 | return h !== null; 858 | })); 859 | }); 860 | var result = {}; 861 | ohHoursWithoutConds.forEach(function (h) { 862 | result[h] = interval; 863 | }); 864 | result = Object.assign(result, condIntervals); 865 | return result; 866 | } 867 | }, { 868 | key: "intervalConditionalStringToObject", 869 | value: function intervalConditionalStringToObject(intervalConditional) { 870 | var _this4 = this; 871 | 872 | return this._splitMultipleIntervalConditionalString(intervalConditional).map(function (p) { 873 | return _this4._readSingleIntervalConditionalString(p); 874 | }); 875 | } 876 | }, { 877 | key: "_intervalConditionObjectToIntervalByDays", 878 | value: function _intervalConditionObjectToIntervalByDays(intervalConditionalObject) { 879 | var result = []; 880 | var itvByDay = {}; 881 | intervalConditionalObject.forEach(function (itv) { 882 | Object.entries(itv.applies).forEach(function (e) { 883 | var _e3 = _slicedToArray(e, 2), 884 | day = _e3[0], 885 | hours = _e3[1]; 886 | 887 | if (!itvByDay[day]) { 888 | itvByDay[day] = {}; 889 | } 890 | 891 | hours.forEach(function (h) { 892 | itvByDay[day][h] = itv.interval; 893 | }); 894 | }); 895 | }); 896 | Object.entries(itvByDay).forEach(function (e) { 897 | var _e4 = _slicedToArray(e, 2), 898 | day = _e4[0], 899 | intervals = _e4[1]; 900 | 901 | if (Object.keys(intervals).length > 0) { 902 | var ident = result.filter(function (r) { 903 | return deepEqual(r.intervals, intervals); 904 | }); 905 | 906 | if (ident.length === 1) { 907 | ident[0].days.push(day); 908 | } else { 909 | result.push({ 910 | days: [day], 911 | intervals: intervals 912 | }); 913 | } 914 | } 915 | }); 916 | result.forEach(function (itv) { 917 | return itv.days.sort(function (a, b) { 918 | return DAYS_ID.indexOf(a) - DAYS_ID.indexOf(b); 919 | }); 920 | }); 921 | result.sort(function (a, b) { 922 | return DAYS_ID.indexOf(a.days[0]) - DAYS_ID.indexOf(b.days[0]); 923 | }); 924 | return result; 925 | } 926 | }, { 927 | key: "_splitMultipleIntervalConditionalString", 928 | value: function _splitMultipleIntervalConditionalString(intervalConditional) { 929 | if (intervalConditional.match(/\(.*\)/)) { 930 | var semicolons = intervalConditional.split("").map(function (c, i) { 931 | return c === ";" ? i : null; 932 | }).filter(function (i) { 933 | return i !== null; 934 | }); 935 | var cursor = 0; 936 | var stack = []; 937 | 938 | while (semicolons.length > 0) { 939 | var scid = semicolons[0]; 940 | var part = intervalConditional.substring(cursor, scid); 941 | 942 | if (part.match(/^[^\(\)]$/) || part.match(/\(.*\)/)) { 943 | stack.push(part); 944 | cursor = scid + 1; 945 | } 946 | 947 | semicolons.shift(); 948 | } 949 | 950 | stack.push(intervalConditional.substring(cursor)); 951 | return stack.map(function (p) { 952 | return p.trim(); 953 | }).filter(function (p) { 954 | return p.length > 0; 955 | }); 956 | } else { 957 | return intervalConditional.split(";").map(function (p) { 958 | return p.trim(); 959 | }).filter(function (p) { 960 | return p.length > 0; 961 | }); 962 | } 963 | } 964 | }, { 965 | key: "_readSingleIntervalConditionalString", 966 | value: function _readSingleIntervalConditionalString(intervalConditional) { 967 | var result = {}; 968 | var parts = intervalConditional.split("@").map(function (p) { 969 | return p.trim(); 970 | }); 971 | 972 | if (parts.length !== 2) { 973 | throw new Error("Conditional interval can't be parsed : " + intervalConditional); 974 | } 975 | 976 | result.interval = this.intervalStringToMinutes(parts[0]); 977 | 978 | if (parts[1].match(/^\(.*\)$/)) { 979 | parts[1] = parts[1].substring(1, parts[1].length - 1); 980 | } 981 | 982 | result.applies = new OpeningHoursParser(parts[1]).getTable(); 983 | return result; 984 | } 985 | }, { 986 | key: "intervalStringToMinutes", 987 | value: function intervalStringToMinutes(interval) { 988 | interval = interval.trim(); 989 | 990 | if (/^\d{1,2}:\d{2}:\d{2}$/.test(interval)) { 991 | var parts = interval.split(":").map(function (t) { 992 | return parseInt(t); 993 | }); 994 | return parts[0] * 60 + parts[1] + parts[2] / 60; 995 | } else if (/^\d{1,2}:\d{2}$/.test(interval)) { 996 | var _parts = interval.split(":").map(function (t) { 997 | return parseInt(t); 998 | }); 999 | 1000 | return _parts[0] * 60 + _parts[1]; 1001 | } else if (/^\d+$/.test(interval)) { 1002 | return parseInt(interval); 1003 | } else { 1004 | throw new Error("Interval value can't be parsed : " + interval); 1005 | } 1006 | } 1007 | }, { 1008 | key: "minutesToIntervalString", 1009 | value: function minutesToIntervalString(minutes) { 1010 | if (typeof minutes !== "number") { 1011 | throw new Error("Parameter minutes is not a number"); 1012 | } 1013 | 1014 | var h = Math.floor(minutes / 60); 1015 | var m = Math.floor(minutes % 60); 1016 | var s = Math.round((minutes - h * 60 - m) * 60); 1017 | return [h, m, s].map(function (t) { 1018 | return t.toString().padStart(2, "0"); 1019 | }).join(":"); 1020 | } 1021 | }]); 1022 | 1023 | return TransportHours; 1024 | }(); 1025 | 1026 | module.exports = TransportHours; 1027 | 1028 | },{"./OpeningHoursBuilder":3,"./OpeningHoursParser":4,"array-flat-polyfill":1,"fast-deep-equal":2}]},{},[5])(5) 1029 | }); 1030 | -------------------------------------------------------------------------------- /assets/osmtogeojson.js: -------------------------------------------------------------------------------- 1 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).osmtogeojson=e()}}(function(){return function i(a,u,s){function l(n,e){if(!u[n]){if(!a[n]){var t="function"==typeof require&&require;if(!e&&t)return t(n,!0);if(c)return c(n,!0);var r=new Error("Cannot find module '"+n+"'");throw r.code="MODULE_NOT_FOUND",r}var o=u[n]={exports:{}};a[n][0].call(o.exports,function(e){var t=a[n][1][e];return l(t||e)},o,o.exports,i,a,u,s)}return u[n].exports}for(var c="function"==typeof require&&require,e=0;e(+t.version||0)?e:t:F.merge(e,t)}e("osm-polygon-features").forEach(function(e){if("all"===e.polygon)r[e.key]=!0;else{var t="whitelist"===e.polygon?"included_values":"excluded_values",n={};e.values.forEach(function(e){n[e]=!0}),r[e.key]={},r[e.key][t]=n}});var i;function R(e){function t(e){return e[0]}function n(e){return e[e.length-1]}function r(e,t){return void 0!==e&&void 0!==t&&e.id===t.id}for(var o,i,a,u,s,l,c=[];e.length;)for(o=e.pop().nodes.slice(),c.push(o);e.length&&!r(t(o),n(o));){for(i=t(o),a=n(o),u=0;u=this._config.preview;if(o)f.postMessage({results:n,workerId:b.WORKER_ID,finished:a});else if(U(this._config.chunk)&&!t){if(this._config.chunk(n,this._handle),this._handle.paused()||this._handle.aborted())return void(this._halted=!0);n=void 0,this._completeResults=void 0}return this._config.step||this._config.chunk||(this._completeResults.data=this._completeResults.data.concat(n.data),this._completeResults.errors=this._completeResults.errors.concat(n.errors),this._completeResults.meta=n.meta),this._completed||!a||!U(this._config.complete)||n&&n.meta.aborted||(this._config.complete(this._completeResults,this._input),this._completed=!0),a||n&&n.meta.paused||this._nextChunk(),n}this._halted=!0},this._sendError=function(e){U(this._config.error)?this._config.error(e):o&&this._config.error&&f.postMessage({workerId:b.WORKER_ID,error:e,finished:!1})}}function l(e){var r;(e=e||{}).chunkSize||(e.chunkSize=b.RemoteChunkSize),u.call(this,e),this._nextChunk=n?function(){this._readChunk(),this._chunkLoaded()}:function(){this._readChunk()},this.stream=function(e){this._input=e,this._nextChunk()},this._readChunk=function(){if(this._finished)this._chunkLoaded();else{if(r=new XMLHttpRequest,this._config.withCredentials&&(r.withCredentials=this._config.withCredentials),n||(r.onload=y(this._chunkLoaded,this),r.onerror=y(this._chunkError,this)),r.open(this._config.downloadRequestBody?"POST":"GET",this._input,!n),this._config.downloadRequestHeaders){var e=this._config.downloadRequestHeaders;for(var t in e)r.setRequestHeader(t,e[t])}if(this._config.chunkSize){var i=this._start+this._config.chunkSize-1;r.setRequestHeader("Range","bytes="+this._start+"-"+i)}try{r.send(this._config.downloadRequestBody)}catch(e){this._chunkError(e.message)}n&&0===r.status&&this._chunkError()}},this._chunkLoaded=function(){4===r.readyState&&(r.status<200||400<=r.status?this._chunkError():(this._start+=this._config.chunkSize?this._config.chunkSize:r.responseText.length,this._finished=!this._config.chunkSize||this._start>=function(e){var t=e.getResponseHeader("Content-Range");if(null===t)return-1;return parseInt(t.substring(t.lastIndexOf("/")+1))}(r),this.parseChunk(r.responseText)))},this._chunkError=function(e){var t=r.statusText||e;this._sendError(new Error(t))}}function c(e){var r,n;(e=e||{}).chunkSize||(e.chunkSize=b.LocalChunkSize),u.call(this,e);var s="undefined"!=typeof FileReader;this.stream=function(e){this._input=e,n=e.slice||e.webkitSlice||e.mozSlice,s?((r=new FileReader).onload=y(this._chunkLoaded,this),r.onerror=y(this._chunkError,this)):r=new FileReaderSync,this._nextChunk()},this._nextChunk=function(){this._finished||this._config.preview&&!(this._rowCount=this._input.size,this.parseChunk(e.target.result)},this._chunkError=function(){this._sendError(r.error)}}function p(e){var i;u.call(this,e=e||{}),this.stream=function(e){return i=e,this._nextChunk()},this._nextChunk=function(){if(!this._finished){var e,t=this._config.chunkSize;return t?(e=i.substring(0,t),i=i.substring(t)):(e=i,i=""),this._finished=!i,this.parseChunk(e)}}}function g(e){u.call(this,e=e||{});var t=[],i=!0,r=!1;this.pause=function(){u.prototype.pause.apply(this,arguments),this._input.pause()},this.resume=function(){u.prototype.resume.apply(this,arguments),this._input.resume()},this.stream=function(e){this._input=e,this._input.on("data",this._streamData),this._input.on("end",this._streamEnd),this._input.on("error",this._streamError)},this._checkIsFinished=function(){r&&1===t.length&&(this._finished=!0)},this._nextChunk=function(){this._checkIsFinished(),t.length?this.parseChunk(t.shift()):i=!0},this._streamData=y(function(e){try{t.push("string"==typeof e?e:e.toString(this._config.encoding)),i&&(i=!1,this._checkIsFinished(),this.parseChunk(t.shift()))}catch(e){this._streamError(e)}},this),this._streamError=y(function(e){this._streamCleanUp(),this._sendError(e)},this),this._streamEnd=y(function(){this._streamCleanUp(),r=!0,this._streamData("")},this),this._streamCleanUp=y(function(){this._input.removeListener("data",this._streamData),this._input.removeListener("end",this._streamEnd),this._input.removeListener("error",this._streamError)},this)}function i(m){var a,o,h,r=Math.pow(2,53),n=-r,s=/^\s*-?(\d+\.?|\.\d+|\d+\.\d+)(e[-+]?\d+)?\s*$/,u=/(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))/,t=this,i=0,f=0,d=!1,e=!1,l=[],c={data:[],errors:[],meta:{}};if(U(m.step)){var p=m.step;m.step=function(e){if(c=e,_())g();else{if(g(),0===c.data.length)return;i+=e.data.length,m.preview&&i>m.preview?o.abort():(c.data=c.data[0],p(c,t))}}}function v(e){return"greedy"===m.skipEmptyLines?""===e.join("").trim():1===e.length&&0===e[0].length}function g(){if(c&&h&&(k("Delimiter","UndetectableDelimiter","Unable to auto-detect delimiting character; defaulted to '"+b.DefaultDelimiter+"'"),h=!1),m.skipEmptyLines)for(var e=0;e=l.length?"__parsed_extra":l[i]),m.transform&&(s=m.transform(s,n)),s=y(n,s),"__parsed_extra"===n?(r[n]=r[n]||[],r[n].push(s)):r[n]=s}return m.header&&(i>l.length?k("FieldMismatch","TooManyFields","Too many fields: expected "+l.length+" fields but parsed "+i,f+t):i=r.length/2?"\r\n":"\r"}(e,r)),h=!1,m.delimiter)U(m.delimiter)&&(m.delimiter=m.delimiter(e),c.meta.delimiter=m.delimiter);else{var n=function(e,t,i,r,n){var s,a,o,h;n=n||[",","\t","|",";",b.RECORD_SEP,b.UNIT_SEP];for(var u=0;u=L)return R(!0)}else for(m=M,M++;;){if(-1===(m=a.indexOf(O,m+1)))return i||u.push({type:"Quotes",code:"MissingQuotes",message:"Quoted field unterminated",row:h.length,index:M}),E();if(m===r-1)return E(a.substring(M,m).replace(_,O));if(O!==z||a[m+1]!==z){if(O===z||0===m||a[m-1]!==z){-1!==p&&p=L)return R(!0);break}u.push({type:"Quotes",code:"InvalidQuotes",message:"Trailing quote on quoted field is malformed",row:h.length,index:M}),m++}}else m++}return E();function b(e){h.push(e),d=M}function w(e){var t=0;if(-1!==e){var i=a.substring(m+1,e);i&&""===i.trim()&&(t=i.length)}return t}function E(e){return i||(void 0===e&&(e=a.substring(M)),f.push(e),M=r,b(f),o&&S()),R()}function C(e){M=e,b(f),f=[],g=a.indexOf(I,M)}function R(e){return{data:h,errors:u,meta:{delimiter:D,linebreak:I,aborted:j,truncated:!!e,cursor:d+(t||0)}}}function S(){A(R()),h=[],u=[]}function x(e,t,i){var r={nextDelim:void 0,quoteSearch:void 0},n=a.indexOf(O,t+1);if(t 2 | 3 | 4 | Unroll 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | Logo 17 |

Unroll

18 |
19 | 20 | 21 |
22 | background img 23 |
24 | 25 |
26 | 27 | 28 | 29 |
30 | 31 | 32 | 47 | 48 | 54 | 55 | 59 | 60 |
61 |
62 | 63 |
64 | 65 |
66 |
67 |
68 | 69 |
70 | 71 | 72 | 73 | 74 | 75 |
76 | 77 |
78 |

Mobility open data, proudly crafted by the OpenStreetMap community - Open source tool by Jungle Bus

79 |
80 | 81 |
82 |
83 | 84 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | projects = { 2 | "Abidjan":{ 3 | "line_list":"https://raw.githubusercontent.com/Jungle-Bus/AbidjanTransport_geom_ci/gh-pages/lines.csv", 4 | "format": "osm-transit-extractor", 5 | "qa": true 6 | }, 7 | "IDF":{ 8 | "line_list":"https://raw.githubusercontent.com/Jungle-Bus/ref-fr-STIF/gh-pages/data/lignes.csv", 9 | "format": "prism", 10 | "qa": true 11 | }, 12 | "Kochi":{ 13 | "line_list":"https://raw.githubusercontent.com/Jungle-Bus/KochiTransport_exports_ci/gh-pages/lines_for_unroll.csv", 14 | "format": "prism", 15 | "qa": true 16 | } 17 | } 18 | 19 | async function on_load(){ 20 | await load_translation_strings(); 21 | var project_id = get_parameter_from_url('project'); 22 | var operator_or_network = get_parameter_from_url('operator_or_network'); 23 | 24 | if (project_id){ 25 | if (projects[project_id]["format"] == "osm-transit-extractor"){ 26 | display_from_osm_transit_extractor_csv_list(projects[project_id]["line_list"], projects[project_id]["qa"]) 27 | } 28 | if (projects[project_id]["format"] == "prism"){ 29 | display_from_prism_csv_list(projects[project_id]["line_list"], projects[project_id]["qa"]) 30 | } 31 | } else if (operator_or_network) { 32 | display_from_overpass(false,operator_or_network) 33 | 34 | } 35 | } 36 | 37 | function display_examples(){ 38 | var lines_examples = [ 39 | { 40 | "id": 6929043, 41 | "ref":"3", 42 | "mode":"bus", 43 | "colour":"blue", 44 | "operator":"", 45 | "network":"", 46 | "name":"Bus 3 : Gare de Choisy-Le-Roi ↔ Gare de Villeneuve-Saint-Georges" 47 | }, 48 | { 49 | "id": 10173635, 50 | "ref":"37", 51 | "mode":"bus", 52 | "colour":"grey", 53 | "operator":"", 54 | "network":"", 55 | "comment": i18n_messages["with fare and schedules"], 56 | "name":"bus 37: Gare Sud↔Yopougon Camp Militaire", 57 | }, 58 | { 59 | "id": 8404844, 60 | "ref":" ", 61 | "mode":"bus", 62 | "colour":"grey", 63 | "operator":"", 64 | "network":"", 65 | "comment": i18n_messages["on-demand bus"], 66 | "name":"Bus Filéo Saint-Pathus : Roissypole ↔ Saint-Pathus" 67 | }, 68 | { 69 | "id": 1667801, 70 | "ref":"24", 71 | "mode":"bus", 72 | "colour":"#F78F4B", 73 | "operator":"", 74 | "network":"", 75 | "comment": i18n_messages["night bus"], 76 | "name":"Noctilien N24: Gare de Sartrouville ↔ Châtelet " 77 | }, 78 | { 79 | "id": 3328765, 80 | "ref":"6", 81 | "mode":"subway", 82 | "colour":"#75c695", 83 | "operator":"", 84 | "network":"", 85 | "comment": i18n_messages["with images and wikipedia"], 86 | "name":"Paris Métro line 6", 87 | }, 88 | ] 89 | var lines_table = document.getElementById("lines_table"); 90 | var lines_stats = document.getElementById("lines_stats"); 91 | lines_stats.innerHTML = ""; 92 | display_table(lines_examples, lines_table) 93 | lines_table.scrollIntoView(); 94 | } 95 | 96 | function display_from_overpass(use_geo, search_network_or_operator_from_url){ 97 | if (use_geo){ 98 | var town = document.getElementById('search_town').value; 99 | if (!town){ 100 | console.error("no town") 101 | var error_town = document.getElementById("error_town"); 102 | error_town.innerHTML = `

Please enter a town name

` 103 | return 104 | } 105 | var overpass_url = ` 106 | https://overpass-api.de/api/interpreter?data=[out:json]; 107 | area[name="${town}"]; 108 | relation 109 | ["type"="route"] 110 | (area) 111 | ; 112 | rel(br)["type"="route_master"]; 113 | out tags; 114 | ` 115 | 116 | } else { 117 | var network = document.getElementById('search_network').value || search_network_or_operator_from_url; 118 | var ref = document.getElementById('search_ref').value; 119 | var error_network_ref = document.getElementById("error_network_ref"); 120 | if (!network && !ref){ 121 | console.error("no network and ref") 122 | error_network_ref.innerHTML = `

${i18n_messages["Please enter a line number and a network"]}

` 123 | return 124 | } 125 | var overpass_url = `https://overpass-api.de/api/interpreter?data=[out:json];relation[type=route_master]` 126 | if (network){ 127 | overpass_url += `[~"network|operator"~"${network}",i]` 128 | } 129 | if (ref) { 130 | overpass_url += `["ref"~"^${ref}$",i]` 131 | } 132 | overpass_url += `;out tags;` 133 | } 134 | var lines_table = document.getElementById("lines_table"); 135 | var lines_stats = document.getElementById("lines_stats"); 136 | lines_stats.innerHTML = ""; 137 | lines_table.innerHTML = ` ${i18n_messages["searching routes ..."]}` 138 | lines_stats.scrollIntoView(); 139 | 140 | 141 | fetch(overpass_url) 142 | .then(function(data) { 143 | return data.json() 144 | }) 145 | .then(function(data) { 146 | var lines = [] 147 | for (var line of data['elements']){ 148 | line['tags']['id'] = line['id']; 149 | line['tags']['mode'] = line['tags']['route_master']; 150 | line['tags']['code'] = line['tags']['ref']; 151 | line['tags']["thumbnail"] = ` 152 | 156 | `; 157 | var not_pt_modes = ['bicycle', 'canoe', 'detour', 'fitness_trail', 'foot', 'hiking', 'horse', 'inline_skates', 'mtb', 'nordic_walking', 'pipeline', 'piste', 'power', 'proposed', 'road', 'running', 'ski', 'historic', 'path', 'junction', 'tracks']; 158 | if (!(not_pt_modes.includes(line['tags']['mode']))) { 159 | lines.push(line['tags']) 160 | } 161 | } 162 | if (lines.length != 0){ 163 | display_table(lines, lines_table) 164 | lines_stats.innerHTML = display_stats(lines); 165 | lines_stats.scrollIntoView(); 166 | } else { 167 | lines_table.innerHTML = `

${i18n_messages["No results"]}

`; 168 | } 169 | }) 170 | .catch(function(error) { 171 | console.error(error.message); 172 | lines_table.innerHTML = i18n_messages["Oops, something went wrong!"]; 173 | }); 174 | } 175 | 176 | function display_from_osm_transit_extractor_csv_list(url, add_qa_to_url){ 177 | Papa.parse(url, { 178 | download: true, 179 | header: true, 180 | dynamicTyping: true, 181 | complete: function(results) { 182 | results.data.splice(-1, 1); 183 | for (var line of results.data){ 184 | line['id'] = line['line_id'].split(':')[2]; 185 | line["thumbnail"] = ` 186 | 190 | ` 191 | } 192 | display_table(results.data, lines_table, add_qa_to_url) 193 | 194 | var lines_stats = document.getElementById("lines_stats"); 195 | lines_stats.innerHTML = display_stats(results.data); 196 | lines_stats.scrollIntoView(); 197 | } 198 | }); 199 | } 200 | 201 | function display_from_prism_csv_list(url, add_qa_to_url){ 202 | Papa.parse(url, { 203 | download: true, 204 | header: true, 205 | dynamicTyping: true, 206 | complete: function(results) { 207 | results.data.splice(-1, 1); 208 | for (var line of results.data){ 209 | line['id'] = line['line_id'].slice(1); 210 | line["thumbnail"] = ` 211 | 215 | ` 216 | } 217 | display_table(results.data, lines_table, add_qa_to_url) 218 | 219 | var lines_stats = document.getElementById("lines_stats"); 220 | lines_stats.innerHTML = display_stats(results.data); 221 | lines_stats.scrollIntoView(); 222 | } 223 | }); 224 | } 225 | 226 | function display_stats(lines){ 227 | var line_nb = lines.length; 228 | if (line_nb > 10){ 229 | var networks = [...new Set(lines.map(x => x.network))]; 230 | var networks_nb = networks.length; 231 | var operators = [...new Set(lines.map(x => x.operator))]; 232 | var operators_nb = operators.length; 233 | var modes = [...new Set(lines.map(x => x.mode))]; 234 | var route_types = [...new Set(lines.map(x => x.route_master))]; 235 | var modes_nb = Math.max(modes.length, route_types.length); 236 | var template = ` 237 |
238 |
239 |
240 |
241 |
242 |

${line_nb}

243 |
244 |
245 |

${i18n_messages["Routes"]}

246 |
247 |
248 |
249 |
250 |
251 |
252 |

${networks_nb}

253 |
254 |
255 |

${i18n_messages["Networks"]}

256 |
257 |
258 |
259 |
260 |
261 |
262 |

${operators_nb}

263 |
264 |
265 |

${i18n_messages["Operators"]}

266 |
267 |
268 |
269 |
270 |
271 |
272 |

${modes_nb}

273 |
274 |
275 |

${i18n_messages["Modes"]}

276 |
277 |
278 |
279 | ` 280 | } else { 281 | var template = ""; 282 | } 283 | return template 284 | } 285 | 286 | function display_table(lines, line_document_element, display_qa = false){ 287 | if (lines.length > 10){ 288 | var table = new Tabulator(line_document_element, { 289 | data:lines, 290 | maxHeight:"100%", 291 | layout:"fitColumns", 292 | groupBy:"mode", 293 | initialSort:[ 294 | {column:"thumbnail", dir:"asc"}, 295 | ], 296 | pagination:"local", 297 | paginationSize:20, 298 | columns:[ 299 | {title:"", field:"thumbnail", formatter:"html", sorter:"alphanum"}, 300 | {title:i18n_messages["Name"], field:"name", headerFilter:"input"}, 301 | {title:i18n_messages["Operator"], field:"operator", headerFilter:"input"}, 302 | {title:i18n_messages["Network"], field:"network", headerFilter:"input"}, 303 | ], 304 | rowClick:function(e, row){ 305 | var current_line_id = row.getData().id; 306 | window.location.href = `route.html?line=${current_line_id}${display_qa ? "&qa=yes" : ""}`; 307 | }, 308 | }); 309 | } else { 310 | var template = ` 311 | 312 | ` 313 | 314 | for (const tags of lines) { 315 | template+=` 316 | 317 | 324 | 325 | 326 | ` 327 | if (tags["comment"]){ 328 | template+= `` 329 | } 330 | template+=`` 331 | } 332 | template+= ` 333 |
318 | 322 | 323 | ${tags["name"]}${tags["operator"]}${tags["network"]}${tags["comment"]}
` 334 | 335 | line_document_element.innerHTML = template; 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /line_data.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module is part of Unroll tool by Jungle Bus. 3 | * It gets structured data about a public transport line by its id 4 | * needs osmtogeojson (from https://github.com/tyrasd/osmtogeojson) 5 | */ 6 | var line_data = (function() { 7 | var data_last_check; 8 | var line_tags; 9 | var trips = []; 10 | 11 | return { 12 | get_data_age: function(){ 13 | return data_last_check; 14 | }, 15 | get_trips_number: function(){ 16 | return trips.length; 17 | }, 18 | get_tags: function(){ 19 | return line_tags; 20 | }, 21 | get_trips: function(){ 22 | return trips; 23 | }, 24 | /** 25 | * fetch and process data 26 | * return a status 27 | */ 28 | init_from_overpass: async function(line_id){ 29 | try { 30 | var overpass_url = `https://overpass-api.de/api/interpreter?data=[out:json][timeout:25];relation(${line_id});(._;>>;);out;` 31 | var overpass_response = await fetch(overpass_url); 32 | overpass_data = await overpass_response.json(); 33 | 34 | data_last_check = overpass_data['osm3s']['timestamp_osm_base']; 35 | 36 | // extract tags and re-structure Overpass response 37 | var other_relations = {} 38 | for (i = 0; i < overpass_data['elements'].length; i++) { 39 | if (overpass_data['elements'][i]['id'] == line_id) { 40 | var relation = overpass_data['elements'][i]; 41 | line_tags = relation['tags']; 42 | } else if (overpass_data['elements'][i]['type'] == "relation") { 43 | var relation_id = overpass_data['elements'][i]['id']; 44 | other_relations[relation_id] = overpass_data['elements'][i]; 45 | } 46 | } 47 | 48 | if (line_tags['type'] == 'route') { 49 | return "This is not a public transport line, it is a trip. Try again using its parent relation" 50 | } else if (line_tags['type'] != 'route_master') { 51 | return "This is not a public transport line" 52 | } 53 | 54 | //extract trips info, and convert stops and shapes to geojson 55 | var data_as_geojson = osmtogeojson(overpass_data); 56 | for (i = 0; i < relation['members'].length; i++) { 57 | var route_id = relation['members'][i]['ref']; 58 | var route = other_relations[route_id]; 59 | 60 | var geojson_elems = {} 61 | for (j = 0; j < data_as_geojson['features'].length; j++) { 62 | if (data_as_geojson['features'][j]['id'] == "relation/"+route_id) { 63 | var geojson_feature = data_as_geojson['features'][j] 64 | } else { 65 | geojson_elems[data_as_geojson['features'][j]['id']] = data_as_geojson['features'][j] 66 | } 67 | } 68 | 69 | var platform_list_as_geojson = [] 70 | route['members'] 71 | .filter(member => member['role'].startsWith("platform")) 72 | .map(member => platform_list_as_geojson.push(geojson_elems[member['type'] + '/' + member['ref']])); 73 | 74 | var stop_position_list_as_geojson = [] 75 | route['members'] 76 | .filter(member => member['role'].startsWith("stop")) 77 | .map(member => stop_position_list_as_geojson.push(geojson_elems[member['type'] + '/' + member['ref']])); 78 | 79 | var mode = route['tags']['route']; 80 | if (["subway", "tram", "train", "railway"].includes(mode)){ 81 | var stop_list_as_geojson = stop_position_list_as_geojson 82 | } else { 83 | var stop_list_as_geojson = platform_list_as_geojson 84 | } 85 | 86 | trips.push({ 87 | "id": route_id, 88 | "tags": route["tags"], 89 | "shape": geojson_feature, 90 | "stop_list" : stop_list_as_geojson, 91 | }); 92 | } 93 | return "ok" 94 | 95 | } catch (error) { 96 | console.error(error) 97 | return "Oops, something went wrong" 98 | } 99 | } 100 | } 101 | }()); 102 | -------------------------------------------------------------------------------- /load.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Unroll 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | Logo 16 |

Unroll

17 |
18 | 19 |
20 |
21 |

guessing route ...

22 |

Home

23 |
24 | 25 | 26 |
27 | 28 |
29 |

Mobility open data, proudly crafted by the OpenStreetMap community - Open source tool by Jungle Bus

30 |
31 | 32 |
33 |
34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /load.js: -------------------------------------------------------------------------------- 1 | var network = get_parameter_from_url('network'); 2 | var ref = get_parameter_from_url('ref'); 3 | 4 | var qa = get_parameter_from_url('qa'); 5 | 6 | main() 7 | 8 | async function main(){ 9 | await load_translation_strings(); 10 | 11 | if (network && ref){ 12 | var overpass_url = ` 13 | https://overpass-api.de/api/interpreter?data=[out:json];relation[type=route_master] 14 | [~"network|operator"~"${network}",i] 15 | ["ref"="${ref}"];out ids;` 16 | 17 | fetch(overpass_url) 18 | .then(function(data) { 19 | return data.json() 20 | }) 21 | .then(function(data) { 22 | if (data['elements'].length > 0){ 23 | var route_id = data['elements'][0]['id']; 24 | if (qa){ 25 | window.location.href = `route.html?line=${route_id}&qa=${qa}`; 26 | } else { 27 | window.location.href = `route.html?line=${route_id}`; 28 | } 29 | 30 | } else { 31 | status = i18n_messages["No route has been found :("]; 32 | document.getElementById("message").innerHTML = display_error(status); 33 | } 34 | }) 35 | .catch(function(error) { 36 | console.error(error.message); 37 | status = i18n_messages["Oops, something went wrong!"] 38 | document.getElementById("message").innerHTML = display_error(status); 39 | }); 40 | 41 | } else { 42 | status = i18n_messages["Search some route on the home page."] 43 | document.getElementById("message").innerHTML = display_error(status); 44 | } 45 | 46 | } 47 | 48 | function display_error(error_message){ 49 | var template = ` 50 |
51 | ${error_message} 52 |
53 | ` 54 | return template 55 | } 56 | -------------------------------------------------------------------------------- /main.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --jungle-color-blue: #0267b1; 3 | --jungle-color-orange: #fbb81d; 4 | } 5 | .w3-junglebus { 6 | color: var(--jungle-color-orange); 7 | background-color: var(--jungle-color-blue) !important; 8 | } 9 | .w3-text-junglebus { 10 | color: var(--jungle-color-blue) !important; 11 | } 12 | 13 | 14 | .stop_dot { 15 | background-color: #FFF; 16 | border-style: solid; 17 | border-width: 4px; 18 | border-color: #000; 19 | border-radius: 50%; 20 | box-sizing: content-box; 21 | content: ''; 22 | height: 6px; 23 | width: 6px; 24 | position: relative; 25 | display: inline-block; 26 | left: -9px; 27 | top: -3px; 28 | } 29 | 30 | .stop_item { 31 | border-left-style: solid; 32 | border-left-width: 4px; 33 | border-left-color: #000; 34 | height: 26px; 35 | box-sizing: content-box; 36 | } 37 | -------------------------------------------------------------------------------- /print.css: -------------------------------------------------------------------------------- 1 | header, h6 { 2 | display :none; 3 | } 4 | 5 | .remove-on-print { 6 | display :none; 7 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Unroll 2 | 3 | Unroll is a tool for viewing transport routes in OpenStreetMap. It allows you to search for transport routes and to display their details: attributes, trips, timetables, stops, shapes, etc. 4 | 5 | ![screenshot](screenshots/paris.png) 6 | 7 | ![screenshot](screenshots/abidjan.png) 8 | 9 | ![screenshot](screenshots/index.png) 10 | 11 | ## Contribute 12 | 13 | Want to inspect routes in your own language ? [Translations happen here](https://www.transifex.com/jungle-bus/unroll) 14 | 15 | ## Goodies 16 | 17 | * You can add `?qa=yes` to the route page url to display [Osmose issues](https://github.com/Jungle-Bus/transport_mapcss) (if any) about the route. 18 | * On OSM wiki, you can display a link to the Unroll page of a route with the [Unroll template](https://wiki.openstreetmap.org/wiki/Template:Unroll) : `{{Unroll|rel=3328765|label=Metro 6}}` 19 | 20 | ## Credits 21 | 22 | This project has been developed by the [Jungle Bus](http://junglebus.io/) team. 23 | 24 | The code in this repository is under the GPL-3.0 license. 25 | 26 | The front cover image is adapted from a photo of a Transisère bus by Anthony Levrot, License CC BY-SA 4.0 (source picture on [Wikimedia Commons](https://commons.wikimedia.org/wiki/File:Iveco_Evadys_n%C2%B08288_(vue_avant_gauche)_-_Transis%C3%A8re_(Lumbin).jpg)). 27 | 28 | 29 | ![Jungle Bus Logo](https://github.com/Jungle-Bus/resources/raw/master/logo/Logo_Jungle_Bus.png) 30 | -------------------------------------------------------------------------------- /route.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Unroll 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | Logo 18 |

Unroll

19 |
20 | 21 |
22 |
23 | 24 |
25 |
26 |
27 |

loading route ...

28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | 36 |
37 | 38 |

Trips

39 |
40 | 41 |

Credits

42 |
43 | 44 |
Last updated on .
45 |
46 | 47 |
48 | 49 |
50 | 51 |
52 |

Mobility open data, proudly crafted by the OpenStreetMap community - Open source tool by Jungle Bus

53 |
54 | 55 |
56 |
57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /route.js: -------------------------------------------------------------------------------- 1 | var line_id = get_parameter_from_url('line'); 2 | //var line_id = 6117019 //IDF 3 | //var line_id = 10361922 //Abidjan 4 | 5 | var display_osmose_issues = (get_parameter_from_url('qa') == "yes") ? true : false; 6 | 7 | var additional_name_language = get_parameter_from_url('name_l10n'); 8 | 9 | unroll_line(line_id) 10 | 11 | async function unroll_line(line_id){ 12 | await load_translation_strings(); 13 | var status = await line_data.init_from_overpass(line_id); 14 | if (status !="ok"){ 15 | console.error(status); 16 | document.getElementById("error").innerHTML = display_error(status); 17 | } 18 | 19 | var data_age = line_data.get_data_age(); 20 | document.getElementById("credits").innerHTML = display_credits(line_id); 21 | document.getElementById("data_age").textContent = data_age; 22 | 23 | var trip_number = line_data.get_trips_number(); 24 | var line_tags = line_data.get_tags(); 25 | 26 | document.getElementById("line_title").innerHTML = display_line_title(line_tags); 27 | document.getElementById("line_detail").innerHTML = display_line_details(line_tags, trip_number); 28 | document.getElementById("line_schedules").innerHTML = display_line_or_route_schedules(line_tags, line_id); 29 | 30 | get_and_display_wikidata_info(line_tags); 31 | get_and_display_line_fares(line_tags); 32 | get_and_display_on_demand_info(line_id, line_tags); 33 | get_and_display_external_info(line_id, line_tags); 34 | 35 | if (display_osmose_issues){ 36 | var osmose_issues = await get_osmose_issues(line_id) 37 | if (osmose_issues.length > 0){ 38 | document.getElementById("osmose_issues").innerHTML = display_line_or_route_issues(osmose_issues); 39 | } 40 | } 41 | 42 | var trips = line_data.get_trips(); 43 | for (var i = 0; i < trips.length; i++) { 44 | var route_title = document.createElement("h5"); 45 | var route = trips[i]; 46 | route_title.innerHTML = display_route_title(route['tags']); 47 | trip_list.appendChild(route_title); 48 | 49 | var route_map = document.createElement("div"); 50 | route_map.classList.add("w3-container"); 51 | route_map.innerHTML = init_route_map(route['tags'], route["stop_list"], route["id"]); 52 | trip_list.appendChild(route_map); 53 | 54 | var map_id = "map_" + route["id"] 55 | display_route_map(map_id, route['tags']['colour'], route["shape"], route["stop_list"]); 56 | 57 | if (route['tags']['interval']){ 58 | var route_schedule = document.createElement("div"); 59 | route_schedule.classList.add("w3-container"); 60 | route_schedule.innerHTML = display_line_or_route_schedules(route['tags'], route["id"]); 61 | trip_list.appendChild(route_schedule); 62 | } 63 | 64 | if (display_osmose_issues){ 65 | var osmose_issues = await get_osmose_issues(route["id"]) 66 | if (osmose_issues.length > 0){ 67 | var osmose_detail = document.createElement("div"); 68 | osmose_detail.classList.add("w3-container"); 69 | osmose_detail.innerHTML = display_line_or_route_issues(osmose_issues); 70 | trip_list.appendChild(osmose_detail); 71 | } 72 | } 73 | } 74 | 75 | } 76 | 77 | function display_line_title(tags){ 78 | var template = ` 79 |
80 |

81 | 85 | 86 | ${tags['name'] || "??" } 87 |

88 |
89 | ` 90 | return template 91 | } 92 | 93 | function display_line_details(tags, trip_number){ 94 | var additional_detail = ''; 95 | if (tags['wheelchair'] && tags['wheelchair'] != "no"){ 96 | additional_detail += `

${i18n_messages["Wheelchair:"]} ${tags['wheelchair']}

` 97 | } 98 | if (tags['school'] && tags['school'] != "no"){ 99 | additional_detail += `

${i18n_messages["School:"]} ${tags['school']}

` 100 | } 101 | if (tags['tourism'] && tags['tourism'] != "no"){ 102 | additional_detail += `

${i18n_messages["Tourism:"]} ${tags['tourism']}

` 103 | } 104 | if (tags['on_demand'] && tags['on_demand'] != "no"){ 105 | additional_detail += `

${i18n_messages["On demand:"]} ${tags['on_demand']}

` 106 | } 107 | if (tags['bicycle'] && tags['bicycle'] != "no"){ 108 | additional_detail += `

${i18n_messages["Bicycle:"]} ${tags['bicycle']}

` 109 | } 110 | if (additional_detail){ 111 | additional_detail = "
" + additional_detail + "
" 112 | } 113 | 114 | var template = ` 115 |
116 |
117 |
${i18n_messages["Details"]}
118 |
${i18n_messages["Edit details"]}
119 |

${i18n_messages["Network:"]} ${tags['network'] || "??" }

120 |

${i18n_messages["Operator:"]} ${tags['operator'] || "??" }

121 | ${additional_detail} 122 |

${trip_number || "??" } ${i18n_messages["trips"]}

123 |
124 |
125 | ` 126 | return template 127 | } 128 | 129 | function display_line_fares(tags){ 130 | var fare = tags['charge'] 131 | var template = ` 132 |
133 |
134 |
${i18n_messages["Fares"]}
135 |
${i18n_messages["Edit Fares"]}
136 |

${fare}

137 |
138 |
139 | ` 140 | return template 141 | } 142 | 143 | function get_and_display_line_fares(tags){ 144 | if (tags['charge']){ 145 | document.getElementById("line_fares").innerHTML = display_line_fares(tags); 146 | } 147 | } 148 | 149 | function display_line_or_route_schedules(tags, relation_id){ 150 | if (tags['interval'] && tags['opening_hours']){ 151 | var th = new TransportHours(); 152 | var result = th.tagsToHoursObject(tags); 153 | var all_intervals = result['allComputedIntervals'] 154 | if (all_intervals == "invalid"){ 155 | var one_liner = `

${i18n_messages["Invalid schedules"]}

` 156 | } else { 157 | var one_liner = ''; 158 | for (i = 0; i < all_intervals.length; i++) { 159 | var period_name = all_intervals[i]['days'].join(" - "); 160 | one_liner += `

${period_name}

`; 161 | var intervals = {}; 162 | Object.keys(all_intervals[i]['intervals']).sort().forEach(function(key) { 163 | intervals[key] = all_intervals[i]['intervals'][key]; 164 | }); 165 | for (var interval_hours in intervals) { 166 | one_liner += `

${interval_hours} ${intervals[interval_hours]} min

` 167 | } 168 | } 169 | } 170 | } else if (tags['interval']) { 171 | var one_liner = `

${i18n_messages["Every "]}${tags['interval']} min

` 172 | } else if (tags['opening_hours']) { 173 | var one_liner = `

${i18n_messages["Runs on "]}${tags['opening_hours']}

` 174 | } else { 175 | var one_liner = `

${i18n_messages["Unknown schedules"]}

` 176 | } 177 | 178 | var template = ` 179 |
180 |
181 |
${i18n_messages["Schedules"]}
182 |
${i18n_messages["Edit schedules"]}
183 | ${one_liner} 184 |
185 |
186 | ` 187 | return template 188 | } 189 | 190 | function display_line_wikipedia_extract(wikipedia_info){ 191 | var template = ` 192 |
193 |
194 |
${i18n_messages["Wikipedia"]}
195 |
${i18n_messages["Read more on Wikipedia"]}
196 |

` 197 | if (wikipedia_info['image']){ 198 | template += `image from wikimedia commons`; 199 | 200 | } 201 | template += `${wikipedia_info['extract']} ...

202 |
203 |
204 | ` 205 | return template 206 | } 207 | 208 | function display_line_images(commons_images){ 209 | var template = ` 210 |
211 |
212 |
${i18n_messages["Images"]}
213 |
${i18n_messages["See on Wikidata"]}
` 214 | 215 | for (var image of commons_images['images_list']){ 216 | template += `image from wikimedia commons ` 217 | } 218 | template += ` 219 |
220 |
221 | ` 222 | return template 223 | } 224 | 225 | function display_route_title(tags){ 226 | var template = ` 227 |
228 | 234 | 235 |
236 | ` 237 | return template 238 | } 239 | 240 | function display_line_or_route_issues(issues){ 241 | var template = ` 242 |
243 |
244 |
${i18n_messages["Issues"]}
245 | 252 |
253 |
254 | ` 255 | return template 256 | } 257 | 258 | function create_stop_list_for_a_route(stop_list, route_colour) { 259 | var route_colour = route_colour || 'grey'; 260 | var inner_html = '' 261 | for (var i = 0; i < stop_list.length; i++) { 262 | stop = stop_list[i] 263 | if (i != stop_list.length - 1) { 264 | var border_color = route_colour; 265 | } else { // remove the border so the stop list ends with a dot 266 | var border_color = "#FFF"; 267 | } 268 | 269 | var add_name = ""; 270 | if (additional_name_language){ 271 | var add_name = stop['properties']['name:'+ additional_name_language] || ""; 272 | } 273 | 274 | inner_html += `
`; 275 | 276 | inner_html += ` 277 | 278 | ${stop['properties'][`name:${current_language}`] || stop['properties']['name'] || i18n_messages['unamed stop']} ${add_name} 279 | 280 | ` 281 | inner_html += ` 282 |
283 | ` 284 | } 285 | 286 | return inner_html 287 | }; 288 | 289 | function init_route_map(tags, stop_list, relation_id){ 290 | var stop_list = create_stop_list_for_a_route(stop_list, tags['colour']) 291 | var template = ` 292 |
293 |
294 |
${i18n_messages["Map and stops"]}
295 |
${i18n_messages["Edit trip"]}
296 |

${tags['name'] || '??'}

297 |
    298 |
  • ${i18n_messages["Origin:"]} ${tags['from'] || '??'} 299 |
  • ${i18n_messages["Destination:"]} ${tags['to'] || '??'} 300 | ` 301 | if (tags['duration']){ 302 | template += `
  • ${i18n_messages["Travel time:"]} ${tags['duration'] || i18n_messages['unknown']}`; 303 | } 304 | template +=` 305 |
306 |

${stop_list}

307 |
308 |
309 |
310 | ` 311 | return template 312 | } 313 | 314 | function display_route_map(map_id, route_colour, route_geojson, stops_geojson){ 315 | var map = L.map(map_id).setView([48.84702, 2.37705], 14); 316 | 317 | L.tileLayer('https://{s}.tile.osm.org/{z}/{x}/{y}.png', { 318 | opacity: 0.6, 319 | attribution: '© OpenStreetMap contributors' 320 | }).addTo(map); 321 | L.control.scale().addTo(map); 322 | 323 | 324 | var line_weight = 4; 325 | var line_colour = route_colour || "grey"; 326 | var feature_outlines = L.geoJson(route_geojson, { 327 | color: "#000", 328 | weight: line_weight + 4, 329 | offset: 0 330 | }).addTo(map); 331 | var feature_background = L.geoJson(route_geojson, { 332 | color: "#fff", 333 | weight: line_weight + 2, 334 | opacity: 1, 335 | offset: 0 336 | }).addTo(map); 337 | var feature = L.geoJson(route_geojson, { 338 | color: line_colour, 339 | weight: line_weight, 340 | opacity: 1, 341 | offset: 0 342 | }).addTo(map); 343 | 344 | function add_popup(feature, layer) { 345 | layer.bindPopup(`${feature.properties.name || i18n_messages['unamed stop']}`); 346 | } 347 | function display_platforms(feature, latlng) { 348 | var myicon = L.icon({ 349 | iconUrl: 'img/stop.png', 350 | }); 351 | return L.marker(latlng, { 352 | icon: myicon, 353 | iconAnchor: [5, 5], 354 | }) 355 | 356 | } 357 | var feature_platforms = L.geoJson(stops_geojson, { 358 | onEachFeature: add_popup, 359 | pointToLayer: display_platforms 360 | }).addTo(map); 361 | 362 | if (feature.getBounds().isValid()){ 363 | map.fitBounds(feature.getBounds()); 364 | } else { 365 | map.fitBounds(feature_platforms.getBounds()); 366 | } 367 | 368 | } 369 | 370 | function display_credits(relation_id){ 371 | var template = ` 372 |
373 |
374 |
${i18n_messages["See on OpenStreetMap"]}
375 | OSM Logo 376 |

${i18n_messages["This information comes from"]} OpenStreetMap, ${i18n_messages["the free and collaborative map"]}. ${i18n_messages["Join the community to complete or correct the detail of this route!"]}


377 |
378 |
379 | ` 380 | return template 381 | } 382 | 383 | async function get_osmose_issues(relation_id){ 384 | var osmose_base_url = "https://osmose.openstreetmap.fr/" + current_language; 385 | var osmose_url = `${osmose_base_url}/api/0.3/issues?osm_type=relation&osm_id=${relation_id}&full=true` 386 | var osmose_response = await fetch(osmose_url); 387 | var osmose_data = await osmose_response.json(); 388 | var issues = osmose_data['issues']; 389 | osmose_to_display = []; 390 | for (const issue of issues){ 391 | var osmose_map_url = `${osmose_base_url}/map/#item=${issue['item']}&zoom=17&lat=${issue['lat']}&lon=${issue['lon']}&issue_uuid=${issue['id']}` 392 | osmose_to_display.push({ 393 | "osmose_issue_id": `${osmose_base_url}/error/${issue['id']}`, 394 | "osmose_text": issue['title']['auto'], 395 | "osmose_map": osmose_map_url 396 | }) 397 | } 398 | return osmose_to_display 399 | } 400 | 401 | async function get_and_display_wikidata_info(tags){ 402 | var wikidata_id = tags["wikidata"]; 403 | 404 | var operator_wikidata_id = tags["operator:wikidata"]; 405 | var network_wikidata_id = tags["network:wikidata"]; 406 | if (tags["wikipedia"]){ 407 | var wikipedia_url = `https://fr.wikipedia.org/wiki/${tags["wikipedia"]}?uselang=en-US`; 408 | var wikipedia_id = tags["wikipedia"].split(":")[1]; 409 | var wikipedia_lang = tags["wikipedia"].split(":")[0]; 410 | } 411 | 412 | var images = [] 413 | if (wikidata_id){ 414 | var wikidata_url = `https://www.wikidata.org/wiki/Special:EntityData/${wikidata_id}.json` 415 | var wikidata_response = await fetch(wikidata_url); 416 | var wikidata_data = await wikidata_response.json(); 417 | var wikidata_content = wikidata_data['entities'][wikidata_id] 418 | if (wikidata_content['sitelinks']['enwiki']){ 419 | var wikipedia_url = wikidata_content['sitelinks']['enwiki']['url']; 420 | var wikipedia_id = wikidata_content['sitelinks']['enwiki']['title']; 421 | var wikipedia_lang = "en"; 422 | } 423 | if (wikidata_content['sitelinks'][current_language+'wiki']){ 424 | var wikipedia_url = wikidata_content['sitelinks'][current_language+'wiki']['url']; 425 | var wikipedia_id = wikidata_content['sitelinks'][current_language+'wiki']['title']; 426 | var wikipedia_lang = current_language; 427 | } 428 | if (wikidata_content['claims']['P18']){ //image 429 | var image_name = wikidata_content['claims']['P18'][0]['mainsnak']['datavalue']['value'] 430 | var image_url = `https://commons.wikimedia.org/wiki/Special:Redirect/file/${image_name}?width=150` 431 | images.push(image_url) 432 | } 433 | if (wikidata_content['claims']['P154']){ //logo 434 | var image_name = wikidata_content['claims']['P154'][0]['mainsnak']['datavalue']['value'] 435 | var image_url = `https://commons.wikimedia.org/wiki/Special:Redirect/file/${image_name}?width=150` 436 | images.push(image_url) 437 | } 438 | if (wikidata_content['claims']['P137']){ //operator 439 | var operator_wikidata_id = wikidata_content['claims']['P137'][0]['mainsnak']['datavalue']['value']['id'] 440 | } 441 | if (wikidata_content['claims']['P361']){ //network 442 | var network_wikidata_id = wikidata_content['claims']['P361'][0]['mainsnak']['datavalue']['value']['id'] 443 | } 444 | } 445 | if (network_wikidata_id){ 446 | var wikidata_url = `https://www.wikidata.org/wiki/Special:EntityData/${network_wikidata_id}.json` 447 | var wikidata_response = await fetch(wikidata_url); 448 | var wikidata_data = await wikidata_response.json(); 449 | var wikidata_content = wikidata_data['entities'][network_wikidata_id] 450 | if (wikidata_content['claims']['P154']){ //logo 451 | var image_name = wikidata_content['claims']['P154'][0]['mainsnak']['datavalue']['value'] 452 | var image_url = `https://commons.wikimedia.org/wiki/Special:Redirect/file/${image_name}?width=150` 453 | images.push(image_url) 454 | } 455 | if (wikidata_content['claims']['P18']){ //image 456 | var image_name = wikidata_content['claims']['P18'][0]['mainsnak']['datavalue']['value'] 457 | var image_url = `https://commons.wikimedia.org/wiki/Special:Redirect/file/${image_name}?width=150` 458 | images.push(image_url) 459 | } 460 | } 461 | if (operator_wikidata_id){ 462 | var wikidata_url = `https://www.wikidata.org/wiki/Special:EntityData/${operator_wikidata_id}.json` 463 | var wikidata_response = await fetch(wikidata_url); 464 | var wikidata_data = await wikidata_response.json(); 465 | var wikidata_content = wikidata_data['entities'][operator_wikidata_id] 466 | if (wikidata_content['claims']['P154']){ //logo 467 | var image_name = wikidata_content['claims']['P154'][0]['mainsnak']['datavalue']['value'] 468 | var image_url = `https://commons.wikimedia.org/wiki/Special:Redirect/file/${image_name}?width=150` 469 | images.push(image_url) 470 | } 471 | if (wikidata_content['claims']['P18']){ //image 472 | var image_name = wikidata_content['claims']['P18'][0]['mainsnak']['datavalue']['value'] 473 | var image_url = `https://commons.wikimedia.org/wiki/Special:Redirect/file/${image_name}?width=150` 474 | images.push(image_url) 475 | } 476 | } 477 | 478 | if (wikipedia_id){ 479 | var wikipedia_api_url = `https://${wikipedia_lang}.wikipedia.org/api/rest_v1/page/summary/${wikipedia_id}` 480 | var wikipedia_response = await fetch(wikipedia_api_url); 481 | var wikipedia_data = await wikipedia_response.json(); 482 | var wikipedia_extract = wikipedia_data['extract']; 483 | if (wikipedia_extract){ 484 | var wikipedia = { 485 | "url": wikipedia_url, 486 | "image" : images[0], 487 | "extract": wikipedia_extract 488 | } 489 | document.getElementById("line_wikipedia").innerHTML = display_line_wikipedia_extract(wikipedia); 490 | } 491 | } 492 | 493 | wikidata_id = wikidata_id || network_wikidata_id || operator_wikidata_id; 494 | if (images.length > 0){ 495 | var wikidata_and_commons = { 496 | "images_list": images, 497 | "url": `https://www.wikidata.org/wiki/${wikidata_id}` 498 | } 499 | document.getElementById("line_commons").innerHTML = display_line_images(wikidata_and_commons); 500 | } 501 | } 502 | 503 | function get_and_display_on_demand_info(relation_id, tags){ 504 | if (tags['on_demand'] === 'yes') { 505 | var title = i18n_messages["This line has on demand services."]; 506 | } 507 | else if (tags['on_demand'] === 'only') { 508 | var title = i18n_messages["This line is on demand."]; 509 | } 510 | else if (tags['hail_and_ride'] === 'partial' || tags['hail_and_ride'] === 'yes') { 511 | var title = i18n_messages["There are some sections on this route with no fixed stops, where you can get on or off the vehicle anywhere along the road by giving a sign to the driver"]; 512 | } else { 513 | return 514 | } 515 | 516 | 517 | var description = tags['on_demand:description'] || tags['hail_and_ride:description']; 518 | var contact_phone = tags['on_demand:phone'] || tags['phone'] || tags['contact:phone']; 519 | var contact_website = tags['on_demand:website'] || tags['website'] || tags['contact:website']; 520 | 521 | var template = ` 522 |
523 |
524 |
${i18n_messages["On demand conditions"]}
525 |

${title}

` 526 | 527 | if (description) { 528 | template += `

${description}

` 529 | } 530 | if (contact_phone) { 531 | template += `

${contact_phone}

` 532 | } 533 | if (contact_website) { 534 | template += `

${contact_website}

` 535 | } 536 | template += ` 537 |
538 |
539 | ` 540 | document.getElementById("line_on_demand_info").innerHTML = template 541 | } 542 | 543 | function get_and_display_external_info(relation_id, tags){ 544 | if (tags["ref:FR:STIF"]){ 545 | var vianavigo_base_url = "https://me-deplacer.iledefrance-mobilites.fr/en/timetables/bus"; 546 | if (current_language == "fr"){ 547 | vianavigo_base_url = "https://me-deplacer.iledefrance-mobilites.fr/fiches-horaires/bus" 548 | } 549 | var template = ` 550 |
551 |
552 |
${i18n_messages["External links"]}
553 |

554 | vianavigo icon 555 | ${i18n_messages["See the timetable on Île-de-France Mobilités website"]} 556 |

557 |

558 | idfm opendata icon 559 | ${i18n_messages["Compare open data and OpenStreetMap"]} 560 |

561 |
562 |
563 | `; 564 | document.getElementById("line_external_links").innerHTML = template; 565 | } 566 | } 567 | 568 | function display_error(error_message){ 569 | var template = ` 570 |
571 | ${error_message} 572 |
573 | ` 574 | return template 575 | } 576 | -------------------------------------------------------------------------------- /screenshots/abidjan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jungle-Bus/unroll/04e5303c1c840bb7c1c7c33cd245e6ac8fd6b83d/screenshots/abidjan.png -------------------------------------------------------------------------------- /screenshots/index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jungle-Bus/unroll/04e5303c1c840bb7c1c7c33cd245e6ac8fd6b83d/screenshots/index.png -------------------------------------------------------------------------------- /screenshots/paris.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jungle-Bus/unroll/04e5303c1c840bb7c1c7c33cd245e6ac8fd6b83d/screenshots/paris.png -------------------------------------------------------------------------------- /taginfo.json: -------------------------------------------------------------------------------- 1 | { 2 | "data_format": 1, 3 | "project": { 4 | "name": "Unroll", 5 | "description": "A public transport route explorer", 6 | "project_url": "https://github.com/Jungle-Bus/unroll", 7 | "icon_url": "https://raw.githubusercontent.com/Jungle-Bus/unroll/master/img/Logo_Unroll.png", 8 | "contact_name": "Jungle Bus", 9 | "contact_email": "contact@junglebus.io" 10 | }, 11 | "tags": [ 12 | { 13 | "key": "charge", 14 | "object_types": ["relation"], 15 | "description": "Route fares" 16 | }, 17 | { 18 | "key": "contact:phone", 19 | "object_types": ["relation"], 20 | "description": "Phone number (useful if 'on demand' route)" 21 | }, 22 | { 23 | "key": "contact:website", 24 | "object_types": ["relation"], 25 | "description": "Website (useful if 'on demand' route)" 26 | }, 27 | { 28 | "key": "colour", 29 | "object_types": ["relation"], 30 | "description": "Route colour" 31 | }, 32 | { 33 | "key": "duration", 34 | "object_types": ["relation"], 35 | "description": "Trip travel duration" 36 | }, 37 | { 38 | "key": "from", 39 | "object_types": ["relation"], 40 | "description": "Trip origin" 41 | }, 42 | { 43 | "key": "hail_and_ride", 44 | "object_types": ["relation"], 45 | "description": "Does the route have 'hail and ride' sections?" 46 | }, 47 | { 48 | "key": "interval", 49 | "object_types": ["relation"], 50 | "description": "Route frequency" 51 | }, 52 | { 53 | "key": "interval:conditional", 54 | "object_types": ["relation"], 55 | "description": "Route frequency details and exceptions" 56 | }, 57 | { 58 | "key": "name", 59 | "object_types": ["relation"], 60 | "description": "Route name" 61 | }, 62 | { 63 | "key": "network", 64 | "object_types": ["relation"], 65 | "description": "Name of the public transport network " 66 | }, 67 | { 68 | "key": "network:wikidata", 69 | "object_types": ["relation"], 70 | "description": "Used to fetch additional content from Wikimedia project (images, wikipedia extract, etc)" 71 | }, 72 | { 73 | "key": "on_demand", 74 | "object_types": ["relation"], 75 | "description": "Is the route on demand?" 76 | }, 77 | { 78 | "key": "bicycle", 79 | "object_types": ["relation"], 80 | "description": "Can you take your bicycle on this bus/train route?" 81 | }, 82 | { 83 | "key": "on_demand:description", 84 | "object_types": ["relation"], 85 | "description": "Additional details if the route is on demand" 86 | }, 87 | { 88 | "key": "opening_hours", 89 | "object_types": ["relation"], 90 | "description": "Route’s activity hours" 91 | }, 92 | { 93 | "key": "operator", 94 | "object_types": ["relation"], 95 | "description": "Name of the public transport operator" 96 | }, 97 | { 98 | "key": "operator:wikidata", 99 | "object_types": ["relation"], 100 | "description": "Used to fetch additional content from Wikimedia project (images, wikipedia extract, etc)" 101 | }, 102 | { 103 | "key": "ref", 104 | "object_types": ["relation"], 105 | "description": "Reference of the route" 106 | }, 107 | { 108 | "key": "ref:FR:STIF", 109 | "object_types": ["relation"], 110 | "description": "Reference of the route in IDFM open data sets. Used to link to official route map and timetables." 111 | }, 112 | { 113 | "key": "route_master", 114 | "object_types": ["relation"], 115 | "description": "Public transport mode" 116 | }, 117 | { 118 | "key": "school", 119 | "object_types": ["relation"], 120 | "description": "Is it a school route?" 121 | }, 122 | { 123 | "key": "to", 124 | "object_types": ["relation"], 125 | "description": "Trip destination" 126 | }, 127 | { 128 | "key": "tourism", 129 | "object_types": ["relation"], 130 | "description": "Is it a touristic route?" 131 | }, 132 | { 133 | "key": "type", 134 | "value": "route", 135 | "object_types": ["relation"], 136 | "description": "Used to select public transport trips" 137 | }, 138 | { 139 | "key": "type", 140 | "value": "route_master", 141 | "object_types": ["relation"], 142 | "description": "Used to select public transport routes" 143 | }, 144 | { 145 | "key": "wheelchair", 146 | "object_types": ["relation"], 147 | "description": "Wheelchair accessibility of the route" 148 | }, 149 | { 150 | "key": "wikipedia", 151 | "object_types": ["relation"], 152 | "description": "Used to fetch Wikipedia extract about the route. Only used if wikidata tag is not set." 153 | }, 154 | { 155 | "key": "wikidata", 156 | "object_types": ["relation"], 157 | "description": "Used to fetch additional content from Wikimedia project (images, wikipedia extract, etc)" 158 | } 159 | ] 160 | } 161 | --------------------------------------------------------------------------------