WME Quick HN Importer

Quickly add house numbers based on open data sources of house numbers

// ==UserScript==
// @name         WME Quick HN Importer
// @namespace    http://www.wazebelgium.be/
// @version      1.2.11
// @description  Quickly add house numbers based on open data sources of house numbers
// @author       Tom 'Glodenox' Puttemans
// @include      /^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor.*$/
// @connect      www.wazebelgium.be
// @grant        GM_xmlhttpRequest
// ==/UserScript==

/* global W, OpenLayers, I18n, require */

(function() {
  'use strict';

  function getUIHookElement() {
    return document.getElementById('search-autocomplete');
  }

  function init(e) {
    if (e && e.user == null) {
      return;
    }
    if (document.getElementById('user-info') == null) {
      setTimeout(init, 500);
      log('user-info element not yet available, page still loading');
      return;
    }
    if (typeof W === 'undefined' || typeof W.loginManager === 'undefined' || typeof W.prefs === 'undefined' || typeof W.map === 'undefined' || typeof OpenLayers === 'undefined' || getUIHookElement() == null) {
      setTimeout(init, 300);
      return;
    }
    if (!W.loginManager.user) {
      W.loginManager.events.register('login', null, init);
      W.loginManager.events.register('loginStatus', null, init);
      // Double check as event might have triggered already
      if (!W.loginManager.user) {
        return;
      }
    }

    var currentStreetId = null;
    var streetNames = {};
    var layer = new OpenLayers.Layer.Vector('Quick HN importer', {
      uniqueName: 'quick-hn-importer',
      styleMap: new OpenLayers.StyleMap({
        "default": new OpenLayers.Style({
          fillColor: '${fillColor}',
          fillOpacity: '${opacity}',
          fontColor: '#111111',
          fontWeight: 'bold',
          strokeColor: '#ffffff',
          strokeOpacity: '${opacity}',
          strokeWidth: 2,
          pointRadius: '${radius}',
          label: '${number}',
          title: '${title}'
        }, {
          context: {
            fillColor: (feature) => feature.attributes && feature.attributes.street == currentStreetId ? '#99ee99' : '#cccccc',
            radius: (feature) => feature.attributes && feature.attributes.number ? Math.max(feature.attributes.number.length * 6, 10) : 10,
            opacity: (feature) => feature.attributes && feature.attributes.street == currentStreetId && feature.attributes.processed ? 0.3 : 1,
            title: (feature) => feature.attributes && feature.attributes.number && feature.attributes.street ? streetNames[feature.attributes.street] + ' ' + feature.attributes.number : ''
          }
        })
      }),
    });
    I18n.translations[I18n.currentLocale()].layers.name['quick-hn-importer'] = 'Quick HN Importer';
    layer.setVisibility(false);
    W.map.addLayer(layer);

    var exitMessage = document.createElement('div');
    exitMessage.style.position = 'absolute';
    exitMessage.style.top = '35px';
    exitMessage.style.width = '100%';
    exitMessage.style.pointerEvents = 'none';
    exitMessage.style.display = 'none';
    exitMessage.innerHTML = `<div style="margin:0 auto; max-width:200px; text-align:center; background:rgba(0, 0, 0, 0.5); color:white; border-radius:3px; padding:5px 15px;">Press ESC to stop adding house numbers</div>`;
    document.getElementById('map').appendChild(exitMessage);

    var loadingMessage = document.createElement('div');
    loadingMessage.style.position = 'absolute';
    loadingMessage.style.bottom = '35px';
    loadingMessage.style.width = '100%';
    loadingMessage.style.pointerEvents = 'none';
    loadingMessage.style.display = 'none';
    loadingMessage.innerHTML = `<div style="margin:0 auto; max-width:300px; text-align:center; background:rgba(0, 0, 0, 0.5); color:white; border-radius:3px; padding:5px 15px;"><i class="fa fa-pulse fa-spinner"></i> Loading address points</div>`;
    document.getElementById('map').appendChild(loadingMessage);

    var streets = {}; // Container for all currently loaded street names
    var updateLayer = () => {
      var segmentSelection = W.selectionManager.getSegmentSelection();
      if (!segmentSelection.segments || segmentSelection.segments.length == 0) {
        return;
      }
      loadingMessage.style.display = null;
      var bounds = null;
      segmentSelection.segments.forEach((segment) => bounds == null ? bounds = segment.attributes.geometry.getBounds() : bounds.extend(segment.attributes.geometry.getBounds()));
      GM_xmlhttpRequest({
        method: "GET",
        url: `https://www.wazebelgium.be/quick-hn-import/?left=${Math.floor(bounds.left - 200)}&top=${Math.floor(bounds.top + 200)}&right=${Math.floor(bounds.right + 200)}&bottom=${Math.floor(bounds.bottom - 200)}`,
        onload: function(response){
          var features = [];
          var currentHouseNumbers = getSelectionHNs();
          response.responseText.split("\n").forEach((line) => {
            var values = line.split(',');
            if (values.length == 4) { // House number
              features.push(new OpenLayers.Feature.Vector(new OpenLayers.Geometry.Point(values[0], values[1]), {
                number: values[2],
                street: values[3],
                processed: currentHouseNumbers.indexOf(values[2]) != -1
              }));
            } else if (values.length == 2) { // Street name
              streets[values[1]] = values[0];
              streetNames[values[0]] = values[1];
            }
          });
          var streetIds = segmentSelection.segments[0].attributes.streetIDs;
          streetIds.push(segmentSelection.segments[0].attributes.primaryStreetID);
          var selectedStreetNames = W.model.streets.getByIds(streetIds).map((street) => street.attributes.name);
          var matchingStreetName = selectedStreetNames.find((streetName) => streets[streetName] != undefined);
          currentStreetId = streets[matchingStreetName];
          layer.addFeatures(features);
          loadingMessage.style.display = 'none';
        },
        onerror: (error) => {
          console.error('Error', error);
          loadingMessage.style.display = 'none';
        }
      });
    };

    var editButtons = getUIHookElement().parentNode;
    var menuToggle = document.createElement('wz-checkbox');
    menuToggle.checked = false;
    menuToggle.style.display = 'none';
    menuToggle.style.alignItems = 'center';
    menuToggle.textContent = "Quick HN importer";
    menuToggle.addEventListener('click', (e) => {
      if (layer.features.length == 0) {
        updateLayer();
      }
      layer.setVisibility(e.target.checked);
      if (e.target.checked) {
        editButtons.querySelector('.add-house-number').click();
      }
    });
    var menuSheet = new CSSStyleSheet();
    menuSheet.replaceSync(`
    label.wz-checkbox slot {
      font-weight: 500;
      font-size: 14px;
      letter-spacing: 0.3px;
      font-family: "Waze Boing", "Waze Boing HB", "Rubik", sans-serif;
    }
    `);
    menuToggle.shadowRoot.adoptedStyleSheets.push(menuSheet);
    getUIHookElement().after(menuToggle);

    var houseNumbersLayer = null;
    // Observe the house number markers to automatically insert the data
    var houseNumberObserver = new MutationObserver((mutations) => {
      if (!menuToggle.checked) {
        exitMessage.style.display = 'none';
        return;
      }
      exitMessage.style.display = houseNumbersLayer.querySelector('div.content.active.new') ? 'block' : 'none';
      let changeTally = 0;
      mutations.forEach((mutation) => {
        if (mutation.type == 'childList') {
          changeTally += mutation.addedNodes.length - mutation.removedNodes.length;
        } else if (mutation.type == 'attributes') {
          if (mutation.target.classList.contains('content') && !mutation.target.classList.contains('new') && mutation.target.classList.contains('active')) {
            var numberInput = mutation.target.querySelector('input.number');
            if (numberInput.value == '') { // Do not interfere when adjusting an existing house number
              // Find nearest house number
              var houseNumberMarkers = W.map.getLayerByName('houseNumbersMapEditorMarkers');
              var locationLonLat = houseNumberMarkers.markers.find((marker) => marker.element.classList.contains('is-active')).lonlat;
              var location = new OpenLayers.Geometry.Point(locationLonLat.lon, locationLonLat.lat);
              var nearestFeature = layer.features.filter((feature) => !feature.attributes.processed).reduce((prev, feature) => prev.geometry.distanceTo(location) > feature.geometry.distanceTo(location) ? feature : prev);
              // Fill in data and prepare for next click
              if (nearestFeature && nearestFeature.geometry.distanceTo(location) < 50) {
                let setValue = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
                setValue.call(numberInput, nearestFeature.data.number);
                // dispatch event so React sees the content as changed
                numberInput.dispatchEvent(new InputEvent('input', { 'bubbles': true }));
                numberInput.blur();
                nearestFeature.attributes.processed = true;
                layer.redraw();
                editButtons.querySelector('.add-house-number').click();
              }
            }
          }
        }
      });
      if (changeTally != 0) {
        // Refresh the processed state when a house number gets removed
        var currentHouseNumbers = getSelectionHNs();
        layer.features.forEach((feature) => feature.attributes.processed = currentHouseNumbers.indexOf(feature.attributes.number) != -1);
        layer.redraw();
      }
    });

    // Observe house number mode to insert the "Quick HN Importer" checkbox
    var menuObserver = new MutationObserver(() => {
      if (editButtons.querySelector('.add-house-number') != null) {
        getUIHookElement().style.display = 'none';
        menuToggle.style.display = 'inline-flex';
        houseNumbersLayer = document.querySelector('div.olLayerDiv.house-numbers-layer');
        houseNumberObserver.observe(houseNumbersLayer, { childList: true, subtree: true, attributes: true });
        if (menuToggle.checked) {
          updateLayer();
          layer.setVisibility(true);
        }
      } else {
        getUIHookElement().style.display = null;
        menuToggle.style.display = 'none';
        layer.setVisibility(false);
        layer.removeAllFeatures();
        streets = {};
        streetNames = {};
      }
    });
    menuObserver.observe(editButtons, { childList: true });

    // Observe the edit panel's contents to add the "Nudge segment" button
    var nudgeButton = document.createElement('button');
    nudgeButton.className = 'action-button waze-btn waze-btn-white';
    nudgeButton.style.marginTop = '14px';
    nudgeButton.textContent = 'Nudge segment';
    nudgeButton.addEventListener('click', () => {
      var UpdateSegmentGeometry = require('Waze/Action/UpdateSegmentGeometry');
      var MoveNode = require("Waze/Action/MoveNode");
      var MultiAction = require("Waze/Action/MultiAction");
      var multiAction = new MultiAction();
      multiAction.setModel(W.model);
      multiAction._description = 'Nudge segment';
      var selectedSegment = W.selectionManager.getSegmentSelection().segments[0];
      if (selectedSegment.geometry.components.length > 2) {
        let newGeometry = selectedSegment.geometry.clone();
        newGeometry.components[1].x += 0.0001;
        multiAction.doSubAction(new UpdateSegmentGeometry(selectedSegment, selectedSegment.geometry.clone(), newGeometry));
      } else {
        var nodeToNudge = W.selectionManager.getSegmentSelection().segments[0].getFromNode();
        var segments = nodeToNudge.getSegmentIds().map((id) => W.model.segments.getObjectById(id));
        var segmentGeometries = {};
        segments.forEach((segment) => {
          let newGeometry = segment.geometry.clone();
          newGeometry.components.filter((component) => component.x == nodeToNudge.geometry.x && component.y == nodeToNudge.geometry.y).x += 0.0001;
          multiAction.doSubAction(new UpdateSegmentGeometry(segment, segment.geometry.clone(), newGeometry));
          segmentGeometries[segment.attributes.id] = segment.geometry.clone();
        });
        let newGeometry = nodeToNudge.geometry.clone();
        newGeometry.x += 0.0001;
        multiAction.doSubAction(new MoveNode(nodeToNudge, nodeToNudge.geometry.clone(), newGeometry, segmentGeometries, {}));
      }
      W.model.actionManager.add(multiAction);
    });
    var editPanelObserver = new MutationObserver(() => {
      if (document.getElementById('edit-panel').style.display == 'none') {
        return;
      }
      var editPanelButtons = document.querySelector('#segment-edit-general .form-group.more-actions');
      if (editPanelButtons) {
        editPanelButtons.appendChild(nudgeButton);
      }
    });
    editPanelObserver.observe(document.getElementById('edit-panel'), { attributes: true });
  }

  function getSelectionHNs() {
    var selectedSegmentIDs = W.selectionManager.getSegmentSelection().segments.map((segment) => segment.attributes.id);
    return W.model.segmentHouseNumbers.getObjectArray().filter((houseNumber) => selectedSegmentIDs.indexOf(houseNumber.attributes.segID) != -1).map((houseNumber) => houseNumber.attributes.number);
  }

  function log(message) {
    if (typeof message === 'string') {
      console.log('%cWME Quick HN Importer: %c' + message, 'color:black', 'color:#d97e00');
    } else {
      console.log('%cWME Quick HN Importer:', 'color:black', message);
    }
  }

  init();
})();