WME Quick HN Importer

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         WME Quick HN Importer
// @namespace    http://www.wazebelgium.be/
// @version      2.0.4
// @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.*$/
// @grant        GM_xmlhttpRequest
// @grant        unsafeWindow
// @connect      geo.api.vlaanderen.be
// @connect      geoservices-urbis.irisnet.be
// @connect      geoservices.wallonie.be
// @connect      service.pdok.nl
// @connect      storitve.eprostor.gov.si
// @require      https://cdn.jsdelivr.net/npm/@turf/[email protected]/turf.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/proj4js/2.19.10/proj4.min.js
// ==/UserScript==

/* global getWmeSdk, turf, proj4 */


let wmeSDK;
const LAYER_NAME = 'Quick HN importer';
(unsafeWindow || window).SDK_INITIALIZED.then(() => {
  wmeSDK = getWmeSdk({ scriptId: "quick-hn-importer", scriptName: "Quick HN Importer"});
  wmeSDK.Events.once({ eventName: "wme-ready" }).then(init);
});

let loadingMessage = document.createElement('div');

let previousCenterLocation = null;
let selectedStreetNames = [];
let streetNumbers = new Map();
let streetNames = new Set();

proj4.defs("EPSG:3794","+proj=tmerc +lat_0=0 +lon_0=15 +k=0.9999 +x_0=500000 +y_0=-5000000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs +type=crs");

let repository = function() {
  let groups = [];
  let directory = new Map();
  let toIndex = (lon, lat) => [ Math.floor(lon * 100), Math.floor(lat * 200) ];
  let toCoord = (x, y) => [ x / 100, y / 200 ];
  let sources = [];
  let getData = (x, y) => {
    let cell = groups[x] ? groups[x][y] : undefined;
    // Data already loaded
    if (cell instanceof Array) {
      return new Promise((resolve, reject) => { resolve(cell) });
    }
    // Data still loading in parallel
    if (cell instanceof Promise) {
      return cell;
    }
    // No data found, start loading
    let promise = new Promise((resolve, reject) => {
      let [ left, top ] = toCoord(x, y);
      let right = left + 0.01,
          bottom = top - 0.005;
      Promise.all(sources.map(source => source(left, bottom, right, top))).then(newFeatureGroups => {
        groups[x][y] = [];
        newFeatureGroups.forEach(newFeatures => newFeatures.forEach(newFeature => {
          groups[x][y].push(newFeature);
          directory.set(newFeature.id, newFeature);
        }));
        resolve([].concat(... newFeatureGroups));
      });
    });
    // Create multidimensional array entry, if needed
    if (!groups[x]) {
      groups[x] = [];
    }
    if (!groups[x][y]) {
      groups[x][y] = promise;
    }
    return promise;
  };

  return {
    addSource: (source) => sources.push(source),
    getExtentData: async function(extent) {
      let features = [];
      let sanityLimit = 10;
      let [ left, bottom ] = toIndex(extent[0], extent[1]),
          [ right, top ] = toIndex(extent[2], extent[3]);
      for (let x = left; x <= right; x += 1) {
        for (let y = top + 1; y >= bottom; y -= 1) {
          sanityLimit--;
          if (sanityLimit <= 0) {
            log("sanity limit reached while retrieving data");
            return;
          }
          features = features.concat(await getData(x, y));
        }
      }
      // Remove duplicate municipality+street+number combinations (mostly boxes at the same location)
      let processedHouseNumbers = new Set();
      return features.filter((feature) => {
        let houseNumberKey = feature.properties.municipality + "|" + feature.properties.street + "|" + feature.properties.number;
        if (!processedHouseNumbers.has(houseNumberKey)) {
          processedHouseNumbers.add(houseNumberKey);
          return true;
        }
        return false;
      });
    },
    cull: () => {
      groups.forEach((col, xIndex) => {
        col.forEach((row, yIndex) => {
          if (turf.distance(toCoord(xIndex, yIndex), Object.values(wmeSDK.Map.getMapCenter())) > 1) {
            row.forEach((feature) => {
              wmeSDK.Map.removeFeatureFromLayer({
                layerName: LAYER_NAME,
                featureId: feature.id
              });
              directory.delete(feature.id);
            });
            col.splice(yIndex, 1);
            if (col.length == 0) {
              groups.splice(xIndex, 1);
            }
          }
        })
      });
    },
    lookup: (featureId) => directory.get(featureId)
  };
}();

// Vlaanderen (Belgium):
repository.addSource((left, bottom, right, top) => {
  let requestedPoly = turf.bboxPolygon([left, bottom, right, top]);
  let regionPoly = turf.polygon([[[4.777969,51.518210],[4.641333,51.422010],[4.537689,51.488676],[4.377120,51.453982],[4.382282,51.381726],[4.217576,51.373885],[3.965949,51.226031],[3.590019,51.305952],[3.414280,51.262159],[3.365430,51.370106],[3.186308,51.362487],[2.545706,51.088757],[2.574734,50.996934],[2.579035,50.918657],[2.606847,50.813426],[2.846267,50.697874],[2.963540,50.773141],[3.120955,50.770274],[3.418007,50.690563],[3.806240,50.747335],[3.920502,50.686118],[4.287230,50.688986],[4.798471,50.772137],[5.135715,50.690628],[5.508128,50.720813],[5.628842,50.773284],[5.821811,50.707623],[5.919873,50.709917],[5.907257,50.769270],[5.694503,50.814860],[5.653214,50.866185],[5.737226,50.906614],[5.866541,51.154922],[5.491785,51.305742],[5.343832,51.276209],[5.071179,51.393485],[5.136526,51.463444],[5.016099,51.491257],[5.002909,51.445380],[4.860834,51.471759],[4.777969,51.518210]]]);
  if (turf.booleanDisjoint(regionPoly, requestedPoly)) {
    return new Promise((resolve, reject) => resolve([]));
  }
  return httpRequest({
    url: `https://geo.api.vlaanderen.be/Adressenregister/ogc/features/v1/collections/Adres/items?f=application/json&bbox=${left},${bottom},${right},${top}`
  }, (response) => {
    let features = [];
    let TYPE_MAPPING = new Map([
      ['InGebruik', 'active'],
      ['Voorgesteld', 'planned']
    ]);
    response.response.features?.forEach((feature) => {
      if (!TYPE_MAPPING.has(feature.properties.AdresStatus)) {
        return;
      }
      features.push({
        type: "Feature",
        id: feature.properties.Id,
        geometry: feature.geometry,
        properties: {
          street: feature.properties.Straatnaam,
          number: feature.properties.Huisnummer,
          municipality: feature.properties.Gemeentenaam,
          type: TYPE_MAPPING.get(feature.properties.AdresStatus)
        }
      });
    });
    return features;
  });
});
// Brussels (Belgium):
repository.addSource((left, bottom, right, top) => {
  let requestedPoly = turf.bboxPolygon([left, bottom, right, top]);
  let regionPoly = turf.polygon([[[4.410507,50.916487],[4.444648,50.883599],[4.420867,50.867712],[4.466045,50.851056],[4.476732,50.820404],[4.452249,50.806449],[4.485805,50.792925],[4.383965,50.761429],[4.331210,50.775508],[4.293584,50.804971],[4.238630,50.814280],[4.253596,50.836364],[4.279553,50.840647],[4.278150,50.866039],[4.295571,50.894146],[4.410507,50.916487]]]);
  if (turf.booleanDisjoint(regionPoly, requestedPoly)) {
    return new Promise((resolve, reject) => resolve([]));
  }
  return httpRequest({
    url: `https://geoservices-urbis.irisnet.be/geoserver/urbisvector/wfs?service=wfs&version=2.0.0&request=GetFeature&typeNames=urbisvector:AddressNumbers&outputFormat=application/json&srsName=EPSG:4326&bbox=${left},${bottom},${right},${top},EPSG:4326`
  }, (response) => {
    let features = [];
    response.response.features?.forEach((feature) => {
      let streetName = feature.properties.STRNAMEFRE || feature.properties.STRNAMEDUT;
      features.push({
        type: "Feature",
        id: feature.properties.INSPIRE_ID,
        geometry: feature.geometry,
        properties: {
          street: streetName,
          number: feature.properties.POLICENUM,
          municipality: feature.properties.MUNNAMEFRE || feature.properties.MUNNAMEDUT,
          type: 'active'
        }
      });
    });
    return features;
  });
});
// Wallonie (Belgium):
repository.addSource((left, bottom, right, top) => {
  let requestedPoly = turf.bboxPolygon([left, bottom, right, top]);
  let regionPoly = turf.polygon([[[5.709641,50.819673],[5.724607,50.758174],[6.025802,50.767641],[6.288645,50.632562],[6.197057,50.530405],[6.351289,50.488288],[6.420534,50.325417],[6.137793,50.129849],[5.999611,50.157914],[5.750798,49.830795],[5.921974,49.705737],[5.898589,49.553056],[5.472410,49.496955],[4.851679,49.793482],[4.781738,49.957938],[4.877586,50.153709],[4.702101,50.095553],[4.692983,49.995491],[4.454353,49.925431],[4.121356,49.959142],[4.147547,50.240543],[4.016592,50.344523],[3.673306,50.295549],[3.615312,50.482215],[3.286056,50.485191],[3.237416,50.688299],[3.055951,50.773557],[2.896000,50.685336],[2.794978,50.732724],[2.982056,50.818491],[3.175681,50.768824],[3.758521,50.780406],[4.249504,50.720289],[4.761160,50.831490],[5.137185,50.719105],[5.709641,50.819673]]]);
  if (turf.booleanDisjoint(regionPoly, requestedPoly)) {
    return new Promise((resolve, reject) => resolve([]));
  }
  return httpRequest({
    url: `https://geoservices.wallonie.be/arcgis/rest/services/DONNEES_BASE/ICAR_ADR_PT/MapServer/1/query?outfields=ADR_ID,ADR_NUMERO,RUE_NM,COM_NM,ADR_FIN&geometryType=esriGeometryEnvelope&inSR=4326&outSR=4326&f=json&geometry=${left},${bottom},${right},${top}`
  }, (response) => {
    let features = [];
    response.response.features?.forEach((feature) => {
      if (feature.attributes.ADR_FIN != null) {
        return;
      }
      let streetName = feature.attributes.RUE_NM.replace(/ \([A-Z]{2}\)$/, "");
      features.push(turf.point([ feature.geometry.x, feature.geometry.y ], {
        street: streetName,
        number: feature.attributes.ADR_NUMERO,
        municipality: feature.attributes.COM_NM,
        type: 'active'
      }, {
        id: feature.attributes.ADR_ID
      }));
    });
    return features;
  });
});
// The Netherlands:
repository.addSource((left, bottom, right, top) => {
  let requestedPoly = turf.bboxPolygon([left, bottom, right, top]);
  let regionPoly = turf.polygon([[[4.276162,51.358135],[3.923522,51.188432],[3.644777,51.247607],[3.368838,51.233552],[3.263139,51.540013],[4.614772,53.286735],[6.399488,53.739490],[7.194590,53.245021],[7.192821,52.998006],[7.052388,52.600208],[6.737035,52.634695],[6.714814,52.461553],[7.071095,52.449966],[7.026320,52.230637],[6.725938,52.035871],[6.869987,51.957545],[6.362073,51.806845],[6.251697,51.852513],[5.946761,51.815520],[6.252633,51.468980],[6.147252,51.152253],[5.960792,51.034575],[6.085199,50.897317],[5.994910,50.749926],[5.681112,50.746043],[5.595992,50.835330],[5.802712,51.128007],[5.435105,51.254632],[5.129234,51.272191],[5.027527,51.476807],[4.910325,51.391902],[4.762117,51.413374],[4.826092,51.460555],[4.764184,51.496237],[4.630135,51.418206],[4.447338,51.433422],[4.401504,51.325998],[4.276162,51.358135]]]);
  if (turf.booleanDisjoint(regionPoly, requestedPoly)) {
    return new Promise((resolve, reject) => resolve([]));
  }
  let transformedPoly = turf.toMercator(requestedPoly);
  let bbox = turf.bbox(transformedPoly, { recompute: true });
  let parseData = async function(response) {
    let features = [];
    response.response.features?.forEach((feature) => {
      features.push({
        type: "Feature",
        id: feature.properties.identificatie,
        geometry: feature.geometry,
        properties: {
          street: feature.properties.openbare_ruimte,
          number: feature.properties.huisnummer.toString(),
          municipality: feature.properties.woonplaats,
          type: 'active'
        }
      });
    });
    if (response.response.features.length > 0) {
      let currentURL = URL.parse(response.finalUrl);
      let moreFeatures = await retrieveData(Number(currentURL.searchParams.get("startIndex")) + 1000);
      features = features.concat(moreFeatures);
    }
    return features;
  };
  let retrieveData = (startIndex) => {
    return httpRequest({
      url: `https://service.pdok.nl/lv/bag/wfs/v2_0?service=wfs&version=2.0.0&request=GetFeature&typeNames=bag:verblijfsobject&outputFormat=application/json&srsName=EPSG:4326&bbox=${bbox.join(",")},EPSG:3857&count=1000&startIndex=${startIndex}`,
      context: startIndex
    }, parseData);
  };
  return retrieveData(0);
});
// Slovenia:
repository.addSource((left, bottom, right, top) => {
  let requestedPoly = turf.bboxPolygon([left, bottom, right, top]);
  let regionPoly = turf.polygon([[[13.559654,45.463437],[13.568005,45.566997],[13.894686,45.631841],[13.546291,45.830907],[13.603082,45.954511],[13.442731,45.984577],[13.621456,46.168312],[13.410116,46.207981],[13.365897,46.300267],[13.731697,46.545804],[14.548483,46.418860],[14.850390,46.601135],[15.061953,46.649556],[15.458807,46.651034],[15.635975,46.717562],[15.934848,46.719517],[15.979947,46.843121],[16.278934,46.878198],[16.325338,46.839441],[16.526141,46.500705],[16.263947,46.515921],[16.285615,46.362069],[16.103550,46.370421],[16.048430,46.291915],[15.620828,46.174993],[15.697987,46.036209],[15.679289,45.820885],[15.286764,45.730688],[15.373769,45.640212],[15.268555,45.601662],[15.371951,45.455085],[15.144787,45.418338],[14.932656,45.506865],[14.820745,45.436712],[14.541802,45.627128],[14.423209,45.465107],[14.000618,45.471789],[13.889001,45.423636],[13.559654,45.463437]]]);
  if (turf.booleanDisjoint(regionPoly, requestedPoly)) {
    return new Promise((resolve, reject) => resolve([]));
  }
  let [slovLeft, slovBottom] = proj4("EPSG:4326", "EPSG:3794", [left, bottom]),
      [slovRight, slovTop] = proj4("EPSG:4326", "EPSG:3794", [right, top]);
  let extractComponent = (feature, componentName) => feature.properties.component.find(component => component["@href"].includes(componentName))["@title"];
  return httpRequest({
    url: `https://storitve.eprostor.gov.si/ows-ins-wfs/ows?service=wfs&version=2.0.0&request=GetFeature&typeNames=ad:Address&outputFormat=application/json&srsName=EPSG:3794&bbox=${slovLeft},${slovBottom},${slovRight},${slovTop},EPSG:3794`
  }, (response) => {
    let features = [];
    response.response.features?.forEach((feature) => {
      features.push(turf.point(proj4("EPSG:3794", "EPSG:4326", feature.properties.position.geometry.coordinates), {
        street: extractComponent(feature, "ad:ThoroughfareName"),
        number: feature.properties.locator.designator.designator,
        municipality: extractComponent(feature, "ad:AddressAreaName"),
        type: 'active'
      }, {
        id: feature.id,
      }));
    });
    return features;
  });
});

function init() {
  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>`;
  wmeSDK.Map.getMapViewportElement().appendChild(loadingMessage);

  previousCenterLocation = Object.values(wmeSDK.Map.getMapCenter());

  // Fix OpenLayers bug where the title tag isn't included in square polygons
  let svgRootContainer = document.querySelector("#WazeMap svg[id*='RootContainer']");
  if (svgRootContainer) {
    new MutationObserver((mutationList) => {
      mutationList.forEach((mutation) => {
        mutation.addedNodes.forEach((element) => {
          if (element.nodeName == "svg" && element.getAttribute("title") != null && element.querySelector("title") == null) {
            let title = document.createElementNS("http://www.w3.org/2000/svg", "title");
            title.textContent = element.getAttribute("title");
            element.appendChild(title);
          }
        });
      })
    }).observe(svgRootContainer, {
      childList: true,
      subtree: true,
    });
  }

  wmeSDK.Map.addLayer({
    layerName: LAYER_NAME,
    styleContext: {
      fillColor: ({ feature }) => feature.properties && !streetNames.has(feature.properties.street.toLowerCase()) ? '#bb3333' : (selectedStreetNames.includes(feature.properties.street.toLowerCase()) ? '#99ee99' : '#fb9c4f'),
      radius: ({ feature }) => feature.properties && feature.properties.number ? Math.max(2 + feature.properties.number.length * 5, 12) : 12,
      opacity: ({ feature }) => feature.properties && streetNumbers.has(feature.properties.street.toLowerCase()) && streetNumbers.get(feature.properties.street.toLowerCase()).has(simplifyNumber(feature.properties.number)) ? 0.3 : 1,
      cursor: ({ feature }) => feature.properties && streetNumbers.has(feature.properties.street.toLowerCase()) && streetNumbers.get(feature.properties.street.toLowerCase()).has(simplifyNumber(feature.properties.number)) ? '' : 'pointer',
      title: ({ feature }) => feature.properties && feature.properties.number && feature.properties.street ? feature.properties.street + ' - ' + feature.properties.number : '',
      number: ({ feature }) => feature.properties && feature.properties.number ? feature.properties.number : ''
    },
    styleRules: [
      {
        style: {
          fillColor: '${fillColor}',
          fillOpacity: '${opacity}',
          fontColor: '#111111',
          fontOpacity: '${opacity}',
          fontWeight: 'bold',
          strokeColor: '#ffffff',
          strokeOpacity: '${opacity}',
          strokeWidth: 2,
          pointRadius: '${radius}',
          graphicName: 'square',
          label: '${number}',
          cursor: '${cursor}',
          title: '${title}'
        }
      }
    ]
  });
  wmeSDK.Map.setLayerVisibility({ layerName: LAYER_NAME, visibility: false });
  wmeSDK.Events.trackLayerEvents({ layerName: LAYER_NAME });

  wmeSDK.Events.trackLayerEvents({ "layerName": "house_numbers" });
  wmeSDK.Events.on({
    eventName: "wme-layer-visibility-changed",
    eventHandler: updateLayer
  });
  wmeSDK.Events.on({
    eventName: "wme-map-move-end",
    eventHandler: () => {
      updateLayer();
      let currentLocation = Object.values(wmeSDK.Map.getMapCenter());
      // Check for any data removal when we're a good distance away
      if (turf.distance(currentLocation, previousCenterLocation) > 1) {
        previousCenterLocation = currentLocation;
        repository.cull();
      }
    }
  });

  wmeSDK.Events.on({
    eventName: "wme-layer-feature-clicked",
    eventHandler: (clickEvent) => {
      let feature = repository.lookup(clickEvent.featureId);
      if (streetNumbers.has(feature.properties.street.toLowerCase()) && streetNumbers.get(feature.properties.street.toLowerCase()).has(simplifyNumber(feature.properties.number))) {
        return;
      }
      // Try to find nearest segment with name match to latch to
      let nearestSegment = findNearestSegment(feature, true);
      if (!nearestSegment) {
        nearestSegment = findNearestSegment(feature, false);
        let nearestStreetName = wmeSDK.DataModel.Streets.getById({ streetId: nearestSegment.primaryStreetId })?.name;
        if (!confirm(`Street name "${feature.properties.street}" could not be found. Do you want to add this number to "${nearestStreetName}"?`)) {
          return;
        }
      }
      wmeSDK.Editing.setSelection({
        selection: {
          ids: [ nearestSegment.id ],
          objectType: "segment"
        }
      });
      // Store house number
      wmeSDK.DataModel.HouseNumbers.addHouseNumber({
        number: feature.properties.number,
        point: feature.geometry,
        segmentId: nearestSegment.id
      });
      // Add to streetNumbers
      let nameMatches = wmeSDK.DataModel.Streets.getAll().filter(street => street.name.toLowerCase() == feature.properties.street.toLowerCase()).length > 0;
      if (nameMatches) {
        if (!streetNumbers.has(feature.properties.street.toLowerCase())) {
          streetNumbers.set(feature.properties.street.toLowerCase(), new Set());
        }
        streetNumbers.get(feature.properties.street.toLowerCase()).add(simplifyNumber(feature.properties.number));
      }
      wmeSDK.Map.redrawLayer({ layerName: LAYER_NAME });
    }
  });
  /* TODO: report that houseNumberId is pretty much useless as there is no way to retrieve a house number somewhere
  wmeSDK.Events.on({
    eventName: "wme-house-number-added",
    eventHandler: (addEvent) => {}
  });
  wmeSDK.Events.on({
    eventName: "wme-house-number-deleted",
    eventHandler: (deleteEvent) => {}
  });*/
  wmeSDK.Events.on({
    eventName: "wme-selection-changed",
    eventHandler: () => {
      let segmentSelection = wmeSDK.Editing.getSelection();
      if (!segmentSelection || segmentSelection.objectType != 'segment' || segmentSelection.ids.length == 0) {
        selectedStreetNames = [];
      } else {
        let streetIds = [];
        segmentSelection.ids.map((segmentId) => wmeSDK.DataModel.Segments.getById({ segmentId: segmentId })).filter(x => x).forEach((segment) => streetIds.push(segment.primaryStreetId, ...segment.alternateStreetIds));
        selectedStreetNames = streetIds.filter(x => x).map((streetId) => wmeSDK.DataModel.Streets.getById({ streetId: streetId })?.name.toLowerCase()).filter(x => x);
      }
      updateLayer();
    }
  });
  // House number tracking
  wmeSDK.Events.trackDataModelEvents({ dataModelName: "segmentHouseNumbers" });
  wmeSDK.Events.trackDataModelEvents({ dataModelName: "streets" });
  wmeSDK.Events.on({
    eventName: "wme-data-model-objects-added",
    eventHandler: (eventData) => {
      if (eventData.dataModelName == "segmentHouseNumbers") {
        eventData.objectIds.forEach(segmentHouseNumber => {
          // Ignore IDs received when adding a house number
          if (Number.isInteger(segmentHouseNumber)) {
            return;
          }
          let segmentId = segmentHouseNumber.substring(0, segmentHouseNumber.indexOf("/"));
          let houseNumber = segmentHouseNumber.substring(segmentId.length + 1);
          let segment = wmeSDK.DataModel.Segments.getById({ segmentId: Number(segmentId) });
          if (!segment) {
            log("Housenumber " + segmentHouseNumber + " could not be matched to segment via the API. Weird, but no blocker");
            return;
          }
          [ segment.primaryStreetId, ... segment.alternateStreetIds ].map(streetId => wmeSDK.DataModel.Streets.getById({ streetId: streetId }).name).forEach(streetName => {
            if (!streetNumbers.has(streetName.toLowerCase())) {
              streetNumbers.set(streetName.toLowerCase(), new Set());
            }
            streetNumbers.get(streetName.toLowerCase()).add(simplifyNumber(houseNumber));
          });
        });
      } else if (eventData.dataModelName == "streets") {
        eventData.objectIds.map(streetId => wmeSDK.DataModel.Streets.getById({ streetId: streetId })).filter(x => x).forEach(street => streetNames.add(street.name.toLowerCase()));
      }
      wmeSDK.Map.redrawLayer({ layerName: LAYER_NAME });
    }
  });
  wmeSDK.Events.on({
    eventName: "wme-data-model-objects-removed",
    eventHandler: (eventData) => {
      if (eventData.dataModelName == "segmentHouseNumbers") {
        eventData.objectIds.forEach(segmentHouseNumber => {
          // Ignore IDs received when removing a house number
          if (Number.isInteger(segmentHouseNumber)) {
            return;
          }
          let segmentId = segmentHouseNumber.substring(0, segmentHouseNumber.indexOf("/"));
          let houseNumber = simplifyNumber(segmentHouseNumber.substring(segmentId.length + 1));
          let segment = wmeSDK.DataModel.Segments.getById({ segmentId: Number(segmentId) });
          if (!segment) {
            log("Housenumber " + segmentHouseNumber + " could not be matched to segment via the API. Weird, but no blocker");
            return;
          }
          [ segment.primaryStreetId, ... segment.alternateStreetIds ].map(streetId => wmeSDK.DataModel.Streets.getById({ streetId: streetId })?.name).forEach(streetName => {
            if (streetName == null || !streetNumbers.has(streetName.toLowerCase())) {
              return;
            }
            streetNumbers.get(streetName.toLowerCase())?.delete(houseNumber);
            if (streetNumbers.get(streetName.toLowerCase())?.delete(houseNumber).size == 0) {
              streetNumbers.delete(streetName.toLowerCase());
            }
          });
        });
      } else if (eventData.dataModelName == "streets") {
        eventData.objectIds.map(streetId => wmeSDK.DataModel.Streets.getById({ streetId: streetId })).filter(x => x).forEach(street => streetNames.delete(street.name.toLowerCase()));
      }
    }
  });
  updateLayer();
}

function updateLayer() {
  if (!wmeSDK.Map.isLayerVisible({ layerName: "house_numbers"}) || wmeSDK.Map.getZoomLevel() < 19) {
    wmeSDK.Map.setLayerVisibility({ layerName: LAYER_NAME, visibility: false });
    return;
  } else if (wmeSDK.Map.isLayerVisible({ layerName: "house_numbers"}) && wmeSDK.Map.getZoomLevel() >= 19 && !wmeSDK.Map.isLayerVisible({ layerName: LAYER_NAME})) {
    wmeSDK.Map.setLayerVisibility({ layerName: LAYER_NAME, visibility: true });
  }
  loadingMessage.style.display = null;
  repository.getExtentData(wmeSDK.Map.getMapExtent()).then((features) => {
    if (features.length > 0) {
      wmeSDK.Map.removeAllFeaturesFromLayer({
        layerName: LAYER_NAME
      });
      wmeSDK.Map.addFeaturesToLayer({
        layerName: LAYER_NAME,
        features: features
      });
    }
    loadingMessage.style.display = 'none';
  });
}

function findNearestSegment(feature, matchName) {
  let streetIds = wmeSDK.DataModel.Streets.getAll().filter(street => street.name.toLowerCase() == feature.properties.street.toLowerCase()).map(street => street.id);
  if (!matchName || streetIds.length > 0) {
    let nearestSegment = wmeSDK.DataModel.Segments.getAll()
      .filter(segment => !matchName || streetIds.includes(segment.primaryStreetId) || streetIds.filter(streetId => segment.alternateStreetIds?.includes(streetId)).length > 0)
      .reduce((current, contender) => {
      contender.distance = turf.pointToLineDistance(feature.geometry, contender.geometry);
      return current.distance < contender.distance ? current : contender;
    }, { distance: Infinity });
    return nearestSegment.distance == Infinity ? null : nearestSegment;
  }
  return null;
}

function simplifyNumber(number) {
  return number.replace(/[\/-]/, "_");
}

function httpRequest(params, process) {
  return new Promise((resolve, reject) => {
    let defaultParams = {
      method: "GET",
      responseType: 'json',
      onload: response => resolve(process(response)),
      onerror: error => reject(error)
    };
    Object.keys(params).forEach(param => defaultParams[param] = params[param]);
    GM_xmlhttpRequest(defaultParams);
  });
}

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);
  }
}