WME Road Events Data

Retrieve and show road events

目前為 2021-02-18 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name        WME Road Events Data
// @namespace   http://www.tomputtemans.com/
// @description Retrieve and show road events
// @include     /^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor.*$/
// @version     1.7.3
// @connect     api.gipod.vlaanderen.be
// @connect     *
// @grant       GM_xmlhttpRequest
// ==/UserScript==

/* global I18n, W, $, OpenLayers */

(function() {
  var UI = {},
    RoadEvents;

  function roadEventsInit() {
    var userInfo = document.getElementById('user-info');

    // Check initialisation
    if (typeof W == 'undefined' || typeof I18n == 'undefined') {
      setTimeout(roadEventsInit, 660);
      log('Waze object unavailable, map still loading');
      return;
    }
    if (userInfo === null) {
      setTimeout(roadEventsInit, 660);
      log('User info unavailable, map still loading');
      return;
    }
    var navTabs = userInfo.querySelector('.nav-tabs');
    if (navTabs === null) {
      setTimeout(roadEventsInit, 660);
      log('Nav tabs unavailable, map still loading');
      return;
    }
    log('Road Events initated');

    // Set translation strings
    addTranslations();

    if (!localStorage.WME_RoadEventsData) {
      var data = {};
      data.disabledSources = [];
      localStorage.WME_RoadEventsData = JSON.stringify(data);
    }

    UI.Tab = function() {
      log("Configuring UI");
      var tabContent = userInfo.querySelector('.tab-content'),
          roadEventsTab = document.createElement('li'),
          roadEventsContent = document.createElement('div'),
          roadEventsFooter = document.createElement('p');

      roadEventsTab.innerHTML = '<a href="#sidepanel-roadEvents" data-toggle="tab">' + I18n.t('road_events.tab_name') + '</a>';
      roadEventsContent.id = 'sidepanel-roadEvents';
      roadEventsContent.className = 'tab-pane';
      navTabs.appendChild(roadEventsTab);
      tabContent.appendChild(roadEventsContent);
      roadEventsFooter.appendChild(document.createTextNode(GM_info.script.name + ': v' + GM_info.script.version));
      roadEventsFooter.style.fontSize = '11px';
      roadEventsFooter.style.marginTop = '1em';
      roadEventsContent.appendChild(roadEventsFooter);

      return {
        add: function(element) {
          roadEventsContent.insertBefore(element, roadEventsFooter);
        }
      };
    }();
    UI.ResultList = function() {
      var filterPane = document.createElement('div');
      // List to contain results encapsulated in this object
      var ul = document.createElement('ul');
      filterPane.style.display = 'none';

      function parseDate(date) {
        return date.getFullYear() + '/' + zeroPad(date.getMonth()+1) + '/' + zeroPad(date.getDate()) + ' ' + zeroPad(date.getHours()) + ':' + zeroPad(date.getMinutes());
      }

      function createButton(text, handler, title) {
        var button = document.createElement('button');
        button.type = 'button';
        button.className = 'btn btn-default';
        button.innerHTML = text;
        button.addEventListener('click', handler, true);
        if (title) {
          button.title = title;
          $(button).tooltip();
        }
        return button;
      }

      function clearList() {
        while (ul.firstChild) {
          ul.removeChild(ul.firstChild);
        }
      }

      // Add search button
      var searchButton = createButton(I18n.t('road_events.search.button_label'), function() {
        RoadEvents.update();
        return false;
      });
      UI.Tab.add(searchButton);

      // Add filter button
      var filterButton = createButton('<span class="fa" style="font-size:14px"></span>', function() {
        $(filterPane).toggle();
        return false;
      }, I18n.t('road_events.search.filter_title'));
      filterButton.style.marginLeft = '0.4em';
      UI.Tab.add(filterButton);

      // Add clear button
      var clearButton = createButton('<span class="fa" style="font-size:14px"></span>', function() {
        clearList();
        UI.Layer.clear();
        UI.ItemDetail.hide();
        UI.ResultList.show();
        return false;
      }, I18n.t('road_events.results.clear_title'));
      clearButton.className = 'btn btn-danger';
      clearButton.style.marginLeft = '0.2em';
      UI.Tab.add(clearButton);

      // Fill in filter pane
      var filterForm = document.createElement('form');
      filterForm.className = 'form-horizontal';
      var sourceGroup = document.createElement('div');
      sourceGroup.className = 'form-group';
      var generalLabel = document.createElement('label');
      generalLabel.className = 'col-sm-3 control-label';
      generalLabel.appendChild(document.createTextNode('Sources:'));
      sourceGroup.appendChild(generalLabel);
      var sourceList = document.createElement('div');
      sourceList.className = 'col-sm-9';

      sourceGroup.appendChild(sourceList);
      filterForm.appendChild(sourceGroup);
      filterPane.appendChild(filterForm);
      UI.Tab.add(filterPane);

      ul.className = 'result-list';
      ul.style.marginTop = '5px';
      ul.style.paddingLeft = '10px';
      UI.Tab.add(ul);

      return {
        // Populate the result list with an array of events
        fill: function(events) {
          this.clear();
          ul.style.listStyleType = 'decimal';
          events.forEach(function(event, index) {
            var li = document.createElement('li');
            li.className = 'result session-available';
            li.style.fontWeight = 'bold';
            li.style.paddingLeft = '0';
            var head = document.createElement('p');
            head.className = 'title';
            if (event.color) {
              head.style.paddingLeft = '5px';
              head.style.borderLeft = '3px solid ' + event.color;
            }
            head.innerHTML = event.description;
            li.appendChild(head);
            var subhead = document.createElement('p');
            subhead.className = 'additional-info';
            subhead.style.fontWeight = 'normal';
            if (event.color) {
              subhead.style.paddingLeft = '8px';
            }
            subhead.appendChild(document.createTextNode(parseDateTime(event.start)));
            if (event.end) {
              var separator = document.createElement('i');
              separator.className = 'fa fa-fw fa-chevron-right';
              subhead.appendChild(separator);
              subhead.appendChild(document.createTextNode(parseDateTime(event.end)));
            }
            li.appendChild(subhead);
            li.addEventListener('click', function() {
              RoadEvents.show(event, index + 1);
            });
            ul.appendChild(li);
          });
          UI.ItemDetail.hide();
          UI.ResultList.show();
        },
        clear: clearList,
        setStatus: function(status, count) {
          var title, info;

          this.clear();
          ul.style.listStyleType = 'none';
          if (status == 'loading') {
            title = I18n.t('road_events.search.loading_header');
            info = I18n.t('road_events.search.loading_subheader', {count: count});
          } else if (status == 'noResult') {
            title = I18n.t('road_events.results.empty_header');
            info = I18n.t('road_events.results.empty_subheader');
          } else if (status == 'noSources') {
            title = I18n.t('road_events.results.no_sources_header');
            info = I18n.t('road_events.results.no_sources_subheader');
          } else {
            log('Invalid status received: ' + status);
            throw new Error('Invalid status received: ' + status);
          }
          var li = document.createElement('li');
          li.className = 'result';
          li.style.fontWeight = 'bold';
          li.style.paddingLeft = '0';
          var head = document.createElement('p');
          head.className = 'title';
          head.appendChild(document.createTextNode(title));
          li.appendChild(head);
          var subhead = document.createElement('p');
          subhead.className = 'additional-info';
          subhead.style.fontWeight = 'normal';
          subhead.appendChild(document.createTextNode(info));
          li.appendChild(subhead);
          ul.appendChild(li);
          UI.ItemDetail.hide();
          UI.ResultList.show();
        },
        show: function() {
          ul.style.display = 'block';
        },
        hide: function() {
          ul.style.display = 'none';
        },
        addFilter: function(sourceName, enabled, action) {
          var newSource = document.createElement('div');
          newSource.className = 'checkbox';
          var sourceLabel = document.createElement('label');
          var sourceCheckbox = document.createElement('input');
          sourceCheckbox.type = 'checkbox';
          sourceCheckbox.checked = enabled;
          sourceLabel.appendChild(sourceCheckbox);
          sourceLabel.appendChild(document.createTextNode(sourceName));
          newSource.appendChild(sourceLabel);
          sourceCheckbox.addEventListener('click', function(e) { action(this.checked); });
          sourceList.appendChild(newSource);
        }
      };
    }();
    UI.ItemDetail = function() {
      var pane = document.createElement('div');
      pane.style.display = 'none';
      pane.style.marginTop = '8px';
      var details = document.createElement('div');
      var backToList = document.createElement('button');
      var backToListIcon = document.createElement('span');
      var listeners = [];

      function returnToList() {
        UI.Layer.removeType("area");
        UI.ItemDetail.hide();
        UI.ResultList.show();
      }

      backToListIcon.className = 'fa';
      backToListIcon.appendChild(document.createTextNode(''));
      backToListIcon.style.marginRight = '1em';
      backToList.appendChild(backToListIcon);
      backToList.appendChild(document.createTextNode(I18n.t('road_events.detail.back_to_list')));
      backToList.className = 'btn btn-link';
      var backToListBottom = backToList.cloneNode(true);
      backToList.addEventListener('click', returnToList);
      backToListBottom.addEventListener('click', returnToList);
      pane.appendChild(backToList);
      pane.appendChild(details);
      pane.appendChild(backToListBottom);
      UI.Tab.add(pane);

      return {
        set: function(event, index) {
          this.clear();
          var eventTitle  = document.createElement('h3');
          eventTitle.style.borderBottom = '1px solid #cecece';
          eventTitle.textContent = (index ? index + '. ' : '') + event.detail.description;
          details.appendChild(eventTitle);
          var moreInfoHeader = document.createElement('h3');
          moreInfoHeader.style.marginTop = '1em';
          moreInfoHeader.textContent = I18n.t('road_events.detail.more_info');
          var moreInfoDataContainer = document.createElement('fieldset');
          var moreInfoData = document.createElement('p');
          moreInfoDataContainer.appendChild(moreInfoData);
          if (event.start) {
            moreInfoData.innerHTML = '<strong>' + I18n.t('road_events.detail.start_date') + ':</strong> ' + parseDateTime(event.start) + '<br />';
          }
          if (event.end) {
            moreInfoData.innerHTML += '<strong>' + I18n.t('road_events.detail.end_date') + ':</strong> ' + parseDateTime(event.end) + '<br />';
          }
          if (event.id) {
            moreInfoData.innerHTML += '<strong>' + I18n.t('road_events.detail.id') + ':</strong> ' + formatDataField(escapeString(event.id)) + '<br />';
          }
          if (moreInfoData.childNodes.length > 0) {
            details.appendChild(moreInfoHeader);
            details.appendChild(moreInfoData);
          }
          for (var group in event.detail) {
            if (typeof event.detail[group] === 'string') {
              continue;
            }
            var sectionHeader = document.createElement('h3');
            sectionHeader.style.marginTop = '1em';
            sectionHeader.textContent = I18n.t('road_events.detail.' + group);
            details.appendChild(sectionHeader);
            var p = document.createElement('p');
            for (var name in event.detail[group]) {
              var item = event.detail[group][name];
              if (item !== null && item.length !== 0) {
                if (Array.isArray(item)) {
                  p.innerHTML += '<strong>' + I18n.t('road_events.detail.' + name, {count: item.length}) + ":</strong> ";
                  p.innerHTML += item.join(', ');
                } else {
                  p.innerHTML += '<strong>' + I18n.t('road_events.detail.' + name) + ":</strong> ";
                  if (typeof item === 'boolean') {
                    p.innerHTML += I18n.t('road_events.detail.' + (item ? 'yes' : 'no'));
                  } else if (typeof item === 'string' && item.startsWith('http')) {
                    p.innerHTML += '<a href="' + item + '" target="_blank">' + item + '</a>';
                  } else {
                    p.innerHTML += item;
                  }
                }
                p.innerHTML += '<br/>';
              }
            }
            details.appendChild(p);
          }
          UI.ResultList.hide();
          UI.ItemDetail.show();
        },
        clear: function() {
          while (details.firstChild) {
            details.removeChild(details.firstChild);
          }
        },
        show: function() {
          pane.style.display = 'block';
        },
        hide: function() {
          pane.style.display = 'none';
        }
      };
    }();
    UI.Layer = function() {
      var layer = new OpenLayers.Layer.Vector(I18n.t('road_events.layer_label'), {
        styleMap: new OpenLayers.StyleMap({
          'default': new OpenLayers.Style({
            pointRadius: 10,
            strokeColor: '#eee',
            fillColor: '${color}',
            fontColor: '#fff',
            fontWeight: 'bold',
            label: '${index}'
          }),
          'select': new OpenLayers.Style({
            pointRadius: 10,
            strokeColor: '#aaa',
            fillColor: '${color}',
            fontColor: '#fff',
            fontWeight: 'bold',
            label: '${index}'
          })
        })
      });
      W.map.addLayer(layer);

      function makeVector(event) {
        return new OpenLayers.Feature.Vector(
          event.coordinate,
          { type: 'marker', description: event.description, id: event.id, color: event.color, index: event.index.toString() }
        );
      }

      return {
        // Add a vector
        add: function(vector) {
          layer.addFeatures([ vector ]);
        },
        // Add a set of road events to the map at once
        fill: function(events) {
          this.clear();
          if (events) {
            var vectors = events.map(makeVector);
            layer.addFeatures(vectors);
          }
        },
        // Remove all features for which a certain attribute is set
        removeType: function(type) {
          layer.removeFeatures(layer.getFeaturesByAttribute('type', type));
        },
        // Clear the layer by removing all features
        clear: function() {
          layer.removeAllFeatures();
        }
      };
    }();

    RoadEvents = function() {
      var activeEvent = null,
        sources = {};

      // Hook into dialog-container for closure addition prefilling
      var observer = new MutationObserver(function(mutations) {
        if (activeEvent !== null) {
          mutations.forEach(function(mutation) {
            for (var i = 0; i < mutation.addedNodes.length; i++) {
              var node = mutation.addedNodes[i];
              if (node !== Node.ELEMENT_NODE) {
                continue;
              }
              if (node.querySelector('.edit-closure.new')) {
                node.querySelector("input.form-control[name='closure_reason']").value = activeEvent.detail.description;
                if (activeEvent.start) {
                  node.querySelector("input[name='closure_hasStartDate']").checked = true;
                  node.querySelector("input.form-control[name='closure_startDate']").value = activeEvent.start.getFullYear() + '-' + zeroPad(activeEvent.start.getMonth()+1) + '-' + zeroPad(activeEvent.start.getDate());
                  node.querySelector("input.form-control[name='closure_startTime']").value = zeroPad(activeEvent.start.getHours()) + ':' + zeroPad(activeEvent.start.getMinutes());
                }
                if (activeEvent.end) {
                  node.querySelector("input.form-control[name='closure_endDate']").value = activeEvent.end.getFullYear() + '-' + zeroPad(activeEvent.end.getMonth()+1) + '-' + zeroPad(activeEvent.end.getDate());
                  node.querySelector("input.form-control[name='closure_endTime']").value = zeroPad(activeEvent.end.getHours()) + ':' + zeroPad(activeEvent.end.getMinutes());
                }
              }
            }
          });
        }
      });
      observer.observe(document.getElementById('edit-panel'), { childList: true, subtree: true });

      return {
        // Retrieve one specific event from a source
        show: function(event, index) {
          sources[event.source].get(event.id, function(detail) {
            UI.ItemDetail.set(detail, index);
            activeEvent = detail;
            if (detail.vector) {
              UI.Layer.add(detail.vector);
              W.map.zoomToExtent(detail.vector.geometry.getBounds());
            }
          });
        },
        // Update all sources for the current location
        update: function() {
          var promises = [];
          var viewBounds = W.map.getExtent();
          activeEvent = null;
          for (var source in sources) {
            if (!sources[source].disabled && sources[source].intersects(viewBounds)) {
              promises.push(sources[source].update());
            }
          }
          if (promises.length === 0) {
            UI.ResultList.setStatus('noSources');
            return;
          }
          UI.ResultList.setStatus('loading', promises.length);
          Promise.all(promises).then(function(results) {
            var roadEvents = results.reduce(function(prev, curr) {
              return prev.concat(curr);
            }).sort(function(a, b) {
              if (a.hindrance == b.hindrance) {
                return new Date(a.start).getTime() - new Date(b.start).getTime();
              } else {
                return (a.hindrance ? -1 : 1);
              }
            }).map(function(roadEvent, index) {
              roadEvent.index = index + 1;
              return roadEvent;
            });
            if (roadEvents.length > 0) {
              UI.ResultList.fill(roadEvents);
              UI.Layer.fill(roadEvents);
            } else {
              UI.ResultList.setStatus('noResult');
            }
          });
        },
        // Add a new source of road events
        addSource: function(source) {
          sources[source.id] = source;
          source.disabled = JSON.parse(localStorage.WME_RoadEventsData).disabledSources.indexOf(source.id) >= 0;
          UI.ResultList.addFilter(source.name, !source.disabled, function(enabled) {
            source.disabled = !enabled;

            var data = JSON.parse(localStorage.WME_RoadEventsData);
            if (enabled) {
              if (data.disabledSources.indexOf(source.id) >= 0) {
                data.disabledSources.splice(data.disabledSources.indexOf(source.id), 1);
              }
            } else {
              data.disabledSources.push(source.id);
            }
            localStorage.WME_RoadEventsData = JSON.stringify(data);
          });
        }
      };
    }();
    window.RoadEvents = RoadEvents;

    // Data source: GIPOD Work Assignments (Flanders, Belgium)
    RoadEvents.addSource(function() {
      var url = 'https://api.gipod.vlaanderen.be/ws/v1/workassignment',
          projection = new OpenLayers.Projection("EPSG:4326"),
          cache = [], // cached event details,
          bounds = new OpenLayers.Bounds(280525, 6557859, 661237, 6712007);

      return {
        id: 'gipod_work',
        name: 'GIPOD Work Assignments',
        intersects: function(view) {
          return bounds.intersectsBounds(view);
        },
        update: function() {
          return new Promise(function(resolve, reject) {
            // Obtain the bounds and transform them to the projection used by GIPOD
            var bounds = W.map.calculateBounds().transform(W.map.getProjectionObject(), projection);
            // bounding box: left bottom coordinate | right top coordinate
            var bbox = bounds.left + "," + bounds.bottom + "|" + bounds.right + "," + bounds.top;
            GM_xmlhttpRequest({
              method: 'GET',
              url: url + '?bbox=' + bbox,
              onload: function(response) {
                var rawData = JSON.parse(response.responseText);
                if (!rawData) {
                  resolve([]);
                  return;
                }
                var roadEvents = rawData.map(function(data) {
                  return {
                    id: escapeString(data.gipodId),
                    source: 'gipod_work',
                    description: escapeString(data.description),
                    start: data.startDateTime,
                    end: data.endDateTime,
                    hindrance: data.importantHindrance,
                    color: (data.importantHindrance ? '#ff3333' : '#ff8c00'),
                    coordinate: new OpenLayers.Geometry.Point(data.coordinate.coordinates[0], data.coordinate.coordinates[1]).transform(projection, W.map.getProjectionObject())
                  };
                });
                resolve(roadEvents);
              },
              onerror: function(xhr, text) {
                resolve([]);
              }
            });
          });
        },
        get: function(gipodId, callback) {
          if (cache[gipodId]) {
            callback(cache[gipodId]);
          } else {
            GM_xmlhttpRequest({
              method: 'GET',
              url: url + '/' + gipodId,
              onload: function(response) {
                var data = JSON.parse(response.responseText);
                if (data.hindrance === null) {
                  data.hindrance = {
                    description: I18n.t('road_events.detail.no_hindrance'),
                    locations: [],
                    effects: []
                  };
                }
                if (data.contactDetails === null) {
                  data.contactDetails = {
                    organisation: I18n.t('road_events.detail.no_organisation')
                  };
                }
                var vector = null;
                if (data.location.geometry !== null) {
                  var poly = null;
                  if (data.location.geometry.type == 'Polygon') {
                    var ring = new OpenLayers.Geometry.LinearRing(data.location.geometry.coordinates[0].map(function(coord) {
                      return new OpenLayers.Geometry.Point(coord[0], coord[1]).transform(projection, W.map.getProjectionObject());
                    }));
                    poly = new OpenLayers.Geometry.Polygon([ ring ]);
                  } else if (data.location.geometry.type == 'MultiPolygon') {
                    var rings = data.location.geometry.coordinates[0].map(function(coords) {
                      return new OpenLayers.Geometry.LinearRing(coords.map(function(coord) {
                        return new OpenLayers.Geometry.Point(coord[0], coord[1]).transform(projection, W.map.getProjectionObject());
                      }));
                    });
                    poly = new OpenLayers.Geometry.Polygon(rings);
                  }
                  if (poly !== null) {
                    vector = new OpenLayers.Feature.Vector(poly, { type: 'area' }, { fillOpacity: 0.6, fillColor: '#ff8c00', strokeColor: '#eeeeee'});
                  }
                }
                var roadEvent = {
                  detail: {
                    description: escapeString(data.description),
                    identification: {
                      periods: [ parseDateTime(escapeString(data.startDateTime)) + ' - ' + parseDateTime(escapeString(data.endDateTime)) ],
                      cities: data.location.cities ? data.location.cities.map(escapeString) : '',
                      comment: escapeString(data.comment),
                      type: escapeString(data.type),
                      state: escapeString(data.state),
                      last_update: escapeString(parseDateTime(data.latestUpdate)),
                      id: escapeString(data.gipodId),
                      source: 'GIPOD Work Assignments'
                    },
                    hindrance: {
                      important_hindrance: data.hindrance.important === true,
                      description: escapeString(data.hindrance.description),
                      direction: escapeString(data.direction),
                      locations: data.hindrance.locations ? data.hindrance.locations.map(escapeString) : '',
                      effects: data.hindrance.effects ? data.hindrance.effects.map(escapeString) : ''
                    },
                    contact: {
                      owner: escapeString(data.owner),
                      contractor: escapeString(data.contactor),
                      main_contractor: escapeString(data.mainContactor),
                      organisation: escapeString(data.contactDetails.organisation),
                      reference: escapeString(data.reference),
                      email: formatDataField(escapeString(data.contactDetails.email)),
                      phone: escapeString(data.contactDetails.phoneNumber1)
                    }
                  },
                  start: new Date(data.startDateTime),
                  end: new Date(data.endDateTime),
                  id: 'http://www.geopunt.be/kaart?app=Hinder_in_kaart_app&lang=nl&GIPODID=' + escapeString(data.gipodId) + '|' + escapeString(data.gipodId),
                  vector: vector,
                  rawData: data
                };
                cache[roadEvent.detail.identification.id] = roadEvent;
                callback(roadEvent);
              }
            });
          }
        }
      };
    }());

    // Data source: GIPOD Manifestations (Flanders, Belgium)
    RoadEvents.addSource(function() {
      // Proxy necessary as this API is not available via a secure connection
      var url = 'https://api.gipod.vlaanderen.be/ws/v1/manifestation',
        projection = new OpenLayers.Projection("EPSG:4326"),
        cache = [], // cached event details
        bounds = new OpenLayers.Bounds(280525, 6557859, 661237, 6712007);

      return {
        id: 'gipod_manifestation',
        name: 'GIPOD Manifestations',
        intersects: function(view) {
          return bounds.intersectsBounds(view);
        },
        update: function() {
          return new Promise(function(resolve, reject) {
            // Obtain the bounds and transform them to the projection used by GIPOD
            var bounds = W.map.calculateBounds().transform(W.map.getProjectionObject(), projection);
            // bounding box: left bottom coordinate | right top coordinate
            var bbox = bounds.left + "," + bounds.bottom + "|" + bounds.right + "," + bounds.top;
            GM_xmlhttpRequest({
              method: 'GET',
              url: url + '?bbox=' + bbox,
              timeout: 10000,
              onload: function(response) {
                var rawData = JSON.parse(response.responseText);
                if (!rawData) {
                  resolve([]);
                  return;
                }
                var roadEvents = rawData.map(function(data) {
                  return {
                    id: escapeString(data.gipodId),
                    source: 'gipod_manifestation',
                    description: escapeString(data.description),
                    start: data.startDateTime,
                    end: data.endDateTime,
                    hindrance: data.importantHindrance,
                    color: (data.importantHindrance ? '#3333ff' : '#008cff'),
                    coordinate: new OpenLayers.Geometry.Point(data.coordinate.coordinates[0], data.coordinate.coordinates[1]).transform(projection, W.map.getProjectionObject())
                  };
                });
                resolve(roadEvents);
              },
              onerror: function(xhr, text) {
                resolve([]);
              }
            });
          });
        },
        get: function(gipodId, callback) {
          if (cache[gipodId]) {
            callback(cache[gipodId]);
          } else {
            GM_xmlhttpRequest({
              method: 'GET',
              url: url + '/' + gipodId,
              onload: function(response) {
                var data = JSON.parse(response.responseText);
                if (data.hindrance === null) {
                  data.hindrance = {
                    description: I18n.t('road_events.detail.no_hindrance'),
                    locations: [],
                    effects: []
                  };
                }
                if (data.contactDetails === null) {
                  data.contactDetails = {
                    organisation: I18n.t('road_events.detail.no_organisation')
                  };
                }
                var vector = null;
                if (data.location.geometry !== null) {
                  var poly = null;
                  if (data.location.geometry.type == 'Polygon') {
                    var ring = new OpenLayers.Geometry.LinearRing(data.location.geometry.coordinates[0].map(function(coord) {
                      return new OpenLayers.Geometry.Point(coord[0], coord[1]).transform(projection, W.map.getProjectionObject());
                    }));
                    poly = new OpenLayers.Geometry.Polygon([ ring ]);
                  } else if (data.location.geometry.type == 'MultiPolygon') {
                    var rings = data.location.geometry.coordinates[0].map(function(coords) {
                      return new OpenLayers.Geometry.LinearRing(coords.map(function(coord) {
                        return new OpenLayers.Geometry.Point(coord[0], coord[1]).transform(projection, W.map.getProjectionObject());
                      }));
                    });
                    poly = new OpenLayers.Geometry.Polygon(rings);
                  }
                  if (poly != null) {
                    vector = new OpenLayers.Feature.Vector(poly, { type: 'area' }, { fillOpacity: 0.6, fillColor: '#ff8c00', strokeColor: '#eeeeee'});
                  }
                }
                var roadEvent = {
                  detail: {
                    description: escapeString(data.description),
                    identification: {
                      periods: data.periods.map(function(period) {return parseDateTime(escapeString(period.startDateTime)) + ' - ' + parseDateTime(escapeString(period.endDateTime));}),
                      cities: data.location.cities ? data.location.cities.map(escapeString) : '',
                      comment: escapeString(data.comment),
                      event_type: escapeString(data.eventType),
                      state: escapeString(data.state),
                      url: escapeString(data.url),
                      id: escapeString(data.gipodId),
                      source: 'GIPOD Manifestations'
                    },
                    hindrance: {
                      important_hindrance: data.hindrance.important == true,
                      description: escapeString(data.hindrance.description),
                      direction: escapeString(data.direction),
                      recurrence: escapeString(data.recurrencePattern),
                      locations: escapeString(data.hindrance.locations),
                      effects: escapeString(data.hindrance.effects)
                    },
                    contact: {
                      owner: escapeString(data.owner),
                      initiator: escapeString((data.initiator ? data.initiator.organisation : null)),
                      organisation: escapeString(data.contactDetails.organisation),
                      reference: escapeString(data.reference),
                      email: formatDataField(escapeString(data.contactDetails.email)),
                      phone: escapeString(data.contactDetails.phoneNumber1)
                    }
                  },
                  start: new Date(data.periods ? data.periods[0].startDateTime : null),
                  end: new Date(data.periods ? data.periods[0].endDateTime : null),
                  id: 'http://www.geopunt.be/kaart?app=Hinder_in_kaart_app&lang=nl&GIPODID=' + escapeString(data.gipodId) + '|' + escapeString(data.gipodId),
                  vector: vector,
                  rawData: data
                };
                cache[roadEvent.detail.identification.id] = roadEvent;
                callback(roadEvent);
              }
            });
          }
        }
      };
    }());

    // Data source: Waze alerts by Wazers
    RoadEvents.addSource(function() {
      var projection = new OpenLayers.Projection("EPSG:4326");
      var url = 'https://www.waze.com';
      switch (W.app.getAppRegionCode()) {
        case 'row':
          url += '/row-rtserver/web/TGeoRSS';
          break;
        case 'il':
          url += '/il-rtserver/web/TGeoRSS';
          break;
        default:
          url += '/rtserver/web/TGeoRSS';
      }
      var cache = {};

      return {
        id: 'waze_alerts',
        name: 'Waze livemap alerts',
        intersects: function(view) {
          return true; // Available worldwide
        },
        update: function() {
          return new Promise(function(resolve, reject) {
            var extent = W.map.getExtent().transform(W.map.getProjectionObject(), projection);
            var data = {
              types: "alerts",
              left: extent.left,
              right: extent.right,
              bottom: extent.bottom,
              top: extent.top
            };
            $.ajax({
              url: url,
              data: data,
              dataType: 'json'
            }).done(function(response) {
              var roadEvents = [];
              if (response.alerts) {
                response.alerts.forEach(function(alert) {
                  roadEvents.push({
                    id: alert.id,
                    source: 'waze_alerts',
                    description: escapeString(alert.type),
                    start: alert.pubMillis,
                    hindrance: false,
                    color: '#614051',
                    coordinate: new OpenLayers.Geometry.Point(alert.location.x, alert.location.y).transform(projection, W.map.getProjectionObject())
                  });
                  cache[alert.id] = alert;
                });
              }
              resolve(roadEvents);
            }).fail(function(xhr, text) {
              resolve([]);
            });
          });
        },
        get: function(id, callback) {
          var alert = cache[id],
              description = alert.type;
          if (alert.reportDescription) {
            description += ' ' + alert.reportDescription;
          }
          if (alert.subtype) {
            description += ' ' + alert.subtype;
          }
          callback({
            detail: {
              description: escapeString(description),
              identification: {
                periods: [ new Date(alert.pubMillis).toISOString().substring(0, 10) ],
                id: alert.id
              },
              contact: {
                owner: 'Waze',
                initiator: escapeString(alert.reportBy)
              }
            }
          });
        }
      };
    }());

    // Data source: Brussels Mobility (Brussels, Belgium)
    /*RoadEvents.addSource(function() {
      // Proxy necessary as this API is not available via a secure connection
      var url = 'https://tomputtemans.com/waze-scripts/road-events.php?source=mobiris',
        projection = new OpenLayers.Projection("EPSG:31370"),
        cache = [], // cached events
        bounds = new OpenLayers.Bounds(472208, 6579412, 499191, 6606318);
      // http://www.bruxellesmobilite.irisnet.be/static/mobiris_files/nl/alerts.json*/
  }

  function addTranslations() {
    var strings = {
      en: {
        layer_label: "Road Events",
        tab_name: "RED",
        tab_title: "Road Events Data",
        search: {
          button_label: "Search current area",
          filter_title: "Select data sources",
          loading_header: "Loading...",
          loading_subheader: {
            one: "Retrieving information from 1 service",
            other: "Retrieving information from %{count} services"
          }
        },
        results: {
          empty_header: "No results found",
          empty_subheader: "Please zoom out or pan to another area",
          limit_reached_header: "Some results may have been omitted",
          limit_reached_header: "A service limits the amount of results and this limit has been reached. Zoom in to see all results",
          no_sources_header: "No data sources for this area",
          no_sources_subheader: "There are no data sources configured for this area",
          clear_title: "Clear results"
        },
        detail: {
          back_to_list: "Back to results",
          more_info: "More information",
          cities: {
            one: "City",
            other: "Cities"
          },
          comment: "Comment",
          contact: "Contact",
          contractor: "Contractor",
          description: "Description",
          direction: "Direction",
          effects: {
            one: "Effect",
            other: "Effects"
          },
          email: "E-mail",
          event_type: "Event type",
          hindrance: "Hindrance",
          id: "ID",
          start_date: "Start date",
          end_date: "End date",
          identification: "Identification",
          important_hindrance: "Important hindrance",
          initiator: "Initiator",
          last_update: "Last update",
          locations: {
            one: "Location",
            other: "Locations"
          },
          main_contractor: "Main Contractor",
          no: "no",
          no_hindrance: "no hindrance specified",
          no_organisation: "no organisation specified",
          organisation: "Organisation",
          owner: "Owner",
          periods: {
            one: "Period",
            other: "Periods"
          },
          phone: "Phone number",
          recurrence: "Recurrences",
          reference: "Reference",
          source: "Source",
          state: "State",
          type: "Type",
          url: "URL",
          yes: "yes"
        }
      },
      nl: {
        layer_label: "Weggebeurtenissen",
        tab_name: "RED",
        tab_title: "Weggebeurtenissen (Road Events Data)",
        search: {
          button_label: "Gebied doorzoeken",
          filter_title: "Selecteer databronnen",
          loading_header: "Aan het laden...",
          loading_subheader: {
            one: "Informatie aan het opvragen bij 1 dienst",
            other: "Informatie aan het opvragen bij %{count} diensten"
          }
        },
        results: {
          empty_header: "Geen resultaten gevonden",
          empty_subheader: "Zoom uit of ga naar een andere locatie",
          limit_reached_header: "Mogelijk ontbreken sommige resultaten",
          limit_reached_header: "Een dienst beperkt het aantal resultaten en deze limiet werd bereikt. Zoom in om het alle resultaten te zien",
          no_sources_header: "Geen databronnen voor dit gebied",
          no_sources_subheader: "Er zijn geen databronnen ingesteld voor dit gebied",
          clear_title: "Resultaten verwijderen"
        },
        detail: {
          back_to_list: "Terug naar resultaten",
          more_info: "Meer informatie",
          cities: {
            one: "Grondgebied",
            other: "Grondgebied"
          },
          comment: "Commentaar",
          contact: "Contact",
          contractor: "Aannemer",
          description: "Omschrijving",
          direction: "Richting",
          effects: {
            one: "Impact",
            other: "Impact"
          },
          email: "E-mail",
          event_type: "Evenementtype",
          hindrance: "Hinder",
          id: "ID",
          start_date: "Begindatum",
          end_date: "Einddatum",
          identification: "Identificatie",
          important_hindrance: "Ernstige hinder",
          initiator: "Initiatiefnemer",
          last_update: "Laatst bijgewerkt",
          locations: {
            one: "Plaats",
            other: "Plaatsen"
          },
          main_contractor: "Hoofdaannemer",
          no: "nee",
          no_hindrance: "geen hinder vermeld",
          no_organisation: "geen organisatie vermeld",
          organisation: "Organisatie",
          owner: "Beheerder",
          periods: {
            one: "Periode",
            other: "Periodes"
          },
          phone: "Telefoonnummer",
          recurrence: "Herhalingspatroon",
          reference: "Referentie",
          source: "Bron",
          state: "Status",
          type: "Type",
          url: "URL",
          yes: "ja"
        }
      },
      fr: {
        layer_label: "Evénements routiers",
        tab_name: "RED",
        tab_title: "Evénements routiers (Road Events Data)",
        search: {
          button_label: "Cherchez ici",
          filter_title: "Sélectionne sources des données",
          loading_header: "Chargement en cours...",
          loading_subheader: {
            one: "En train de obtenir de l'information chez 1 service",
            other: "En train de obtenir de l'information chez %{count} services"
          }
        },
        results: {
          empty_header: "Aucun résultat trouvé",
          empty_subheader: "Dézoome ou déplace la carte",
          limit_reached_header: "Quelques résultats peuvent manquer",
          limit_reached_header: "Une service limite la quantité de résultats et on a attaint cette limite. Agrandis la carte pour voir tout les résultats",
          no_sources_header: "Aucun source pour cette région",
          no_sources_subheader: "Aucun source de données spécifié pour cette région",
          clear_title: "Supprime les résultats"
        },
        detail: {
          back_to_list: "Retour aux résultats",
          more_info: "Détails",
          cities: {
            one: "Commune",
            other: "Communes"
          },
          comment: "Commentaire",
          contact: "Contact",
          contractor: "Contractant",
          description: "Description",
          direction: "Direction",
          effects: {
            one: "Impact",
            other: "Impact"
          },
          email: "Email",
          event_type: "Type d'événement",
          hindrance: "Obstacles",
          id: "ID",
          start_date: "Date de début",
          end_date: "Date de fin",
          identification: "Identification",
          important_hindrance: "Obstacle majeur",
          initiator: "Initiateur",
          last_update: "Dernier mise à jour",
          locations: {
            one: "Endroit",
            other: "Endroits"
          },
          main_contractor: "Contractant principal",
          no: "non",
          no_hindrance: "aucun obstacle spécifié",
          no_organisation: "aucun organisation spécifié",
          organisation: "Organisation",
          owner: "Administrateur",
          periods: {
            one: "Période",
            other: "Périodes"
          },
          phone: "Numéro telephone",
          recurrence: "Récurrences",
          reference: "Réference",
          source: "Source",
          state: "Etat",
          type: "Type",
          url: "URL",
          yes: "oui"
        }
      }
    };
    strings['en-GB'] = strings['en-US'] = strings.en;
    I18n.locales.get().forEach(function(locale) {
      if (I18n.translations[locale]) {
        I18n.translations[locale].road_events = strings[locale];
      }
    });
  }

  // Input: Unix timestamp or ISO string - Output: 2014-12-22 00:00 in local timezone
  function parseDateTime(datetime) {
    if (isNaN(datetime)) {
      return datetime.replace(/(\d{4})-(\d{2})-(\d{2})T(\d{2}:\d{2})(:\d{2})(\.\d+)?.*/, "$1-$2-$3 $4");
    }
    return new Date(datetime).toISOString().replace(/(\d{4})-(\d{2})-(\d{2})T(\d{2}:\d{2})(:\d{2})(\.\d+)?.*/, "$1-$2-$3 $4");
  }

  function escapeString(text) {
    if (Array.isArray(text)) {
      if (text.length > 0) {
        return text.map(escapeString);
      } else {
        return null;
      }
    }
    if (typeof text === 'undefined' || text === null || text === '' || text === ' ') {
      return null;
    }
    if (typeof text !== 'string') {
      text = JSON.stringify(text);
    }
    return text.replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#x27;');
  }

  function zeroPad(number) {
    return ("0" + number).slice(-2);
  }

  function formatDataField(field) {
    if (typeof field === 'string') {
      if (/^https?:\/\//.test(field)) { // Website
        if (field.indexOf('|') == -1) {
          return '<a href="' + encodeURI(field) + '" target="_blank">' + field + '</a>';
        } else {
          var dataParts = field.split('|');
          return '<a href="' + encodeURI(dataParts[0]) + '" target="_blank">' + dataParts[1] + '</a>';
        }
      } else if (/^[^ ]+@[^ ]+\.[a-zA-Z0-9]+$/.test(field)) { // E-mail
        return '<a href="mailto:' + encodeURI(field) + '" target="_blank">' + field + '</a>';
      } else {
        return field;
      }
    }
    return null;
  }

  // TODO: handle these calls visually for the user
  function showError(error) {
    log(error);
  }

  function log(message) {
    if (typeof message === 'string') {
      console.log('Road Events: ' + message);
    } else {
      console.log('Road Events', message);
    }
  }

  // attempt to bootstrap after about a second
  log('Road Events bootstrap set');
  setTimeout(roadEventsInit, 1010);
})();