Greasy Fork is available in English.

WME Kadastr 🇺🇦

Adds kadastr layer to the map

// ==UserScript==
// @name           WME Kadastr 🇺🇦
// @author         Andrei Pavlenko, Anton Shevchuk, madnut
// @version        2024.07.03.001
// @match          https://beta.waze.com/*editor*
// @match          https://www.waze.com/*editor*
// @exclude        https://www.waze.com/*user/*editor/*
// @grant          none
// @description    Adds kadastr layer to the map
// @require        https://greasyfork.org/scripts/24851-wazewrap/code/WazeWrap.js
// @require        https://greasyfork.org/scripts/450160-wme-bootstrap/code/WME-Bootstrap.js
// @require        https://update.greasyfork.org/scripts/450320/1281847/WME-UI.js
// @namespace      https://greasyfork.org/uk/users/160654-waze-ukraine
// ==/UserScript==

/* jshint esversion: 11 */
/* global $ */
/* global W */
/* global WazeWrap */
/* global WMEUIHelper */
/* global WMEUI */
/* global OpenLayers */
(function () {
  'use strict';

  const NAME = 'kadastr-ua';
  const LOCAL_STORAGE_ITEM = NAME + '-layer';
  const KADASTR_ID = '#' + NAME;
  const SWITCHER_ID = '#layer-switcher-item_map_' + NAME;
  const AREA_DATA_ID = `.${NAME}-area-data`;
  const LOCALITY_ID = `${NAME}-locality-name`;
  const KAD_TITLE_EN = "Kadastr 🇺🇦";
  const KAD_TITLE_UA = "Кадастр 🇺🇦";
  
  const RESOLUTIONS = [156543.03390625,
                       78271.516953125,
                       39135.7584765625,
                       19567.87923828125,
                       9783.939619140625,
                       4891.9698095703125,
                       2445.9849047851562,
                       1222.9924523925781,
                       611.4962261962891,
                       305.74811309814453,
                       152.87405654907226,
                       76.43702827453613,
                       38.218514137268066,
                       19.109257068634033,
                       9.554628534317017,
                       4.777314267158508,
                       2.388657133579254,
                       1.194328566789627,
                       0.5971642833948135,
                       0.298582141697406,
                       0.1,
                       0.05,
                       0.025
                      ];
  
  let isLoaded = false;
  let kadastrLayer, markerLayer, markerIcon;
  let helper, tab;
  let visibility = !!localStorage.getItem(LOCAL_STORAGE_ITEM);
  const markerIconURL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABR5JREFUeNrsXF1uEzEQdtKkSX/TqDwUISCIV6TkGQklHACpPQHhBuEG2xOQnIDkBqk4AImQeC4Sz9DwQh9AZUtp0iZtmakcRMG760121/bujGStlLU93vlmxp/t3aSurq4YiTpJkwkIAAKAhAAgAEgIAAKAhAAgAEgIAAKAhAAgAEgIgERIxqTBHj57UoELlpJDlQMo+1tv3u2b8kwp3c8DwOh1uGxDqUEpSDazofSgdAGMNgHg3+gbcGnwUpizOwSjiQXA+EEAeBsfjW4FYHgREBaA0CQAnL2+C6Uasqo+pjRdokELAPjk2gvB692ioabDZJ1OoPEZ19XjupMLgCLjawWCshQED45cft+v8S9hvKPJBTsdj2/8vpzNsnxmgaVTqVnSUQXS0UHSFmJdP8a3z87Z0WjEzsD4Ivk+HF1fcwBCMZ9nhdyin0jo8gVeMlIQeD/SzLJM3ZPzMft0ZLPDk1+Oxv9bsA7WxTbYVlLKfEzxT0F+Ug8aEj1/rkQPkbC1uqJtKlIRAVZUxp+mLuxLMhVZsY4Avtg6isr4M0ZCMcpFWtQRUJfJ+R7Gx1TRgbID5SkvO/w32y0SJOeEepQGyegEAFLMr+7pAo3cqL7/KPLQbv/xI4ww3Ot5LmqMfT8sFryoap33Ea8UJJN+vp0O/9BJgbwAw7dldAEQaMTXonubS3l2a3lJmzQUZQqqeVU4dk49LVnjo/C6LZ86fI3VRAAqXvx9fHkpujWYkZ1YvO0NQR0S64lK4gA4nUycbrUdcr5XFGCbtk9dsQZgw+3mhdj7p1sW82x3+NElNVbjtyJEMnTwSvDkmffsndoOvSOAJQ6ApIo2AOQWFpwoZWnWPp3aOulKNAAui6N5KGHNp65YA9Bzu4kHKiFsDdR96pIaq6kAHLgD4LgrUoVUsj1D+sE2VZH3u+iSGmssIwDF5RSrDQat+DB+xWkNsLaYDWSsxgHADzoGbnU2nfdorg/QwbA1CePXmMtB/6b3PtAgykOZqCdh10VVNp1mxXzODYS3YOCuKCXhb3gP6zgZH/tGHfOMMWiJ+kAGaeFntzq4Jf3l+KfU+S/IB371PF/Gw/p762syDOhBbCOAP1jfi47eXlmRpYplGeP76LOflDNhKW+VSBdMti+8BjE24wEAD8MJsiNjuNLGOluVYy1CwbY+jN/hY0vEShhfQbdlUsedtVV2F4wowd1vrCmwDbaVTGU2H1PkovLVRHzgV37a4GEKHqyfTSb/Hd5gusplMtdeP0PqeqnquwGlr6cDCDjh3Ve8HYO8v6RKuerNuDpTL0rHoBQAPun1FQ6hr2Li1SkCVHug8ghUDgBf+LQUqG6p+iZAtwiYLoDsCPXZKhZd2gLA30KL0iAWfSUppqX4FkM5ZDUfwPgVXZ5Zt7ciGjHRYSYAnBLuhahiTzXt1D0CwvbQhm4Pqx0AnBruhtD1rg6004QIQGkGTEun/5jCCAB5Whpkumjo+Fc12tHQkGipVrTTlBQU5KTZ0PkBtQYgAFqqHe00LQLm9eCG7g+nPQBz0FItaaeJETClpQMf9Qe60k4jAZhht9TSlXYaRUMFtBQnVK8/9cNjxpopz2TaN2JWQHUIgDloqdtbdR3daafpETD1cNE+kW2a9xsJAKeWIobTNIF2xiECRLTUGNoZCwAEtNQY2mk0DXWgpcwk2vmvZJjZ0jB8/GZHQByE/qyDACAASAgAAoCEACAASAgAAoCEACAASAgAAoCEAEiG/BZgAIdH+4FfAgoVAAAAAElFTkSuQmCC';

  WMEUI.addStyle(`
    #loader-thinking {
      display: inline-block;
      margin: 0;
      padding: 0;
      animation-name: spin;
      animation-duration: 5000ms;
      animation-iteration-count: infinite;
      animation-timing-function: linear;
    }
    @keyframes spin {
      from {
        transform:rotate(0deg);
      }
      to {
        transform:rotate(360deg);
      }
    }
  `);

  $(document)
    .on('bootstrap.wme', ready);

  function ready() {
    if (isLoaded) {
      // ugly fix for script duplications
      return;
    }
    isLoaded = true;
    polyfillOpenLayers();
    createMarkerIcon();
    addKadastrLayer();
    addMarkerLayer();
    createSwitcher();
    createTab();
    addHandlers();
  }

  function createSwitcher() {
    let $ul = $('.collapsible-GROUP_DISPLAY');
    let $li = document.createElement('li');
    let checkbox = document.createElement("wz-checkbox");
    checkbox.id = 'layer-switcher-item_map_' + NAME;
    checkbox.className = "hydrated";
    checkbox.checked = visibility;
    checkbox.appendChild(document.createTextNode(KAD_TITLE_UA));

    $li.append(checkbox);
    $ul.append($li);
  }

  function switchLayer(flag) {
    localStorage.setItem(LOCAL_STORAGE_ITEM, flag ? '1' : '');
    visibility = flag;

    kadastrLayer.setVisibility(flag);
    markerLayer.setVisibility(flag);
    if (flag) {
      $(KADASTR_ID).tab('show');
      $(AREA_DATA_ID).html("Оберіть об'єкт для отримання інформації");
    }
  }

  function createTab() {
    // Setup Tab with options
    helper = new WMEUIHelper(NAME);
    tab = helper.createTab(KAD_TITLE_UA, '');
    let text = visibility
      ? "Оберіть об'єкт для отримання посилання"
      : "Ввімкніть шар кадастру та оберіть об'єкт для отримання інформації";
    tab.addText('area-data', text);
    tab.inject();
  }

  function addKadastrLayer() {
    const maxZoom = 22;
    kadastrLayer = new OpenLayers.Layer.XYZ(
      KAD_TITLE_EN,
      'https://cdn.kadastr.live/tiles/raster/styles/parcels/${z}/${x}/${y}.png',
      {
        sphericalMercator: true,
        isBaseLayer: false,
        visibility: visibility,
        zoomOffset: 12,
        RESOLUTION_PROPERTIES: {},
        resolutions: RESOLUTIONS,
        serverResolutions: RESOLUTIONS.slice(0, maxZoom),
        transitionEffect: "resize",
        attribution: "",
        uniqueName: NAME
      }
    );
    W.map.addLayer(kadastrLayer);
  }

  function addMarkerLayer() {
    markerLayer = new OpenLayers.Layer.Markers(
      'Kadastr marker',
      {
        isBaseLayer: false,
        visibility: visibility
      }
    );
    W.map.addLayer(markerLayer);
  }

  function addHandlers() {
    W.map.events.register('click', null, e => {
      if (!visibility) return false;
      let coordinates = W.map.getLonLatFromPixel(e.xy);
      drawMarker(coordinates);
      prepareLink(coordinates);
      //fetchAreaData(coordinates);
      $(KADASTR_ID).tab('show');
    });

    $(document).on('click', SWITCHER_ID, e => {
      switchLayer(e.target.checked);
    });

    /* name, desc, group, title, shortcut, callback, scope */
    new WazeWrap.Interface.Shortcut(
      KAD_TITLE_EN,
      'Відображення кадастру',
      'layers',
      KAD_TITLE_UA,
      'S+K',
      function () {
        let checked = localStorage.getItem(LOCAL_STORAGE_ITEM);
        switchLayer(!checked);
        $(SWITCHER_ID).prop('checked', !checked);
      },
      null
    ).add();
  }

  function createMarkerIcon() {
    const size = new OpenLayers.Size(50, 50);
    const offset = new OpenLayers.Pixel(-(size.w/2), -size.h*0.8);
    markerIcon = new OpenLayers.Icon(markerIconURL, size, offset);
  }

  function drawMarker(coordinates) {
    let {lon, lat} = coordinates;
    let lonLat = new OpenLayers.LonLat(lon, lat);
    markerLayer.clearMarkers();
    markerLayer.addMarker(new OpenLayers.Marker(lonLat, markerIcon));
  }

  function prepareLink(coordinates) {
    // https://kadastr.live/parcel/4610136300:04:017:0088
    let url = new URL('https://kadastr.live/parcel/');
    //url.searchParams.set('cc', coordinates.lon +','+ coordinates.lat);
    //url.searchParams.set('z', '16');
    //url.searchParams.set('l', 'kadastr');
    //url.searchParams.set('bl', 'ortho10k_all');

    let $area = $(AREA_DATA_ID);
    $area.html('');
    //$area.html('<a href="'+ url.toString() +'" target="_blank">'+ url.hostname +'?cc='+ url.searchParams.get('cc') +'</a>');
    $area.html('<a href="'+ url.toString() +'" target="_blank">'+ url.toString() +'</a>');
  }

  function fetchAreaData(coordinates) {
    // https://cdn.kadastr.live/tiles/maps/kadastr/16/37131/22279.pbf
    let $area = $(AREA_DATA_ID);

    $area.html('<div id="loader-thinking">🤔</div> Завантаження');

    let params = new URLSearchParams();
        params.set('x', coordinates.lat);
        params.set('y', coordinates.lon);
        params.set('zoom', '13');
        params.set('actLayers[]', 'kadastr');

    fetch('https://waze.com.ua/kadastr_api', {
      method: 'POST',
      body: params
    }).then(data => data.json()).then(data => {
      let parcel = data.parcel;
      let district = data.district;
      if (!parcel) {
        $area.html('😕 Ділянку не знайдено');
        return;
      }
      parcel = parcel[0];

      $area.html('');
      $area.append(`
        <div><strong>Ділянка: </strong>${parcel.cadnum}</div>
        <div><strong>Область: </strong>${district.natoobl}</div>
        <div><strong>Населений пункт: </strong><span id="${LOCALITY_ID}">не визначено</span></div>
        <div><strong>Тип власності: </strong>${parcel.ownership}</div>
        <div><strong>Цільове призначення: </strong>${parcel.use}</div>
        <div><strong>Площа: </strong>${parcel.area+' '+parcel.unit_area}</div>
        <div style="margin-top: 10px;"><a target="_blank" style="color: #26bae8; padding: 5px 0;" href="${parcel.linkToOwnershipInfo}">Інформація про ділянку</a></div>
      `);
      getLocalityName(parcel.koatuu, data.ikk.zona);
    }).catch(err => {
      $area.html('⛔ Помилка');
      console.error(err);
    });
  }

  function getLocalityName(koatuu, zoneNumber) {
    fetch(`https://waze.com.ua/kadastr_locality?code=${koatuu}&zone_number=${zoneNumber}`)
      .then(response => response.json())
      .then(data => {
        if (data.name) {
          let localityName = data.name.toLowerCase().replace(/^./, data.name[0].toUpperCase());
          if (/\//.test(localityName)) return;
          $('#' + LOCALITY_ID).html(localityName);
        }
      });
  }

  /**
   * This polyfill is required for OpenLayers.Icon functionality
   * @link ?
   */
  function polyfillOpenLayers() {
    OpenLayers.Icon = OpenLayers.Class({
      url: null,
      size: null,
      offset: null,
      calculateOffset: null,
      imageDiv: null,
      px: null,
      initialize: function initialize(a, b, c, d) {
        this.url = a, this.size = b || {w: 20, h: 20}, this.offset = c || {
          x: -(this.size.w / 2),
          y: -(this.size.h / 2)
        }, this.calculateOffset = d;
        var e = OpenLayers.Util.createUniqueID("OL_Icon_");
        this.imageDiv = OpenLayers.Util.createAlphaImageDiv(e)
      },
      destroy: function destroy() {
        this.erase(), OpenLayers.Event.stopObservingElement(this.imageDiv.firstChild), this.imageDiv.innerHTML = "", this.imageDiv = null
      },
      clone: function clone() {
        return new OpenLayers.Icon(this.url, this.size, this.offset, this.calculateOffset)
      },
      setSize: function setSize(a) {
        null != a && (this.size = a), this.draw()
      },
      setUrl: function setUrl(a) {
        null != a && (this.url = a), this.draw()
      },
      draw: function draw(a) {
        return OpenLayers.Util.modifyAlphaImageDiv(this.imageDiv, null, null, this.size, this.url, "absolute"), this.moveTo(a), this.imageDiv
      },
      erase: function erase() {
        null != this.imageDiv && null != this.imageDiv.parentNode && OpenLayers.Element.remove(this.imageDiv)
      },
      setOpacity: function setOpacity(a) {
        OpenLayers.Util.modifyAlphaImageDiv(this.imageDiv, null, null, null, null, null, null, null, a)
      },
      moveTo: function moveTo(a) {
        null != a && (this.px = a), null != this.imageDiv && (null == this.px ? this.display(!1) : (this.calculateOffset && (this.offset = this.calculateOffset(this.size)), OpenLayers.Util.modifyAlphaImageDiv(this.imageDiv, null, {
          x: this.px.x + this.offset.x,
          y: this.px.y + this.offset.y
        })))
      },
      display: function display(a) {
        this.imageDiv.style.display = a ? "" : "none"
      },
      isDrawn: function isDrawn() {
        return this.imageDiv && this.imageDiv.parentNode && 11 !== this.imageDiv.parentNode.nodeType;
      },
      CLASS_NAME: "OpenLayers.Icon"
    });
  }
})();