housingEnricherNL

A script with the goal of enriching funda.nl and pararius.nl sites with information about the listing from official sources

// ==UserScript==
// @name          housingEnricherNL
// @namespace     com.parker.david
// @version       V0.0.9
// @description   A script with the goal of enriching funda.nl and pararius.nl sites with information about the listing from official sources
// @author        David Parker
// @match         https://www.funda.nl/zoeken/huur*
// @match         https://www.funda.nl/zoeken/koop*
// @match         https://www.pararius.nl/koopwoningen*
// @match         https://www.pararius.nl/huurwoningen*
// @icon          https://www.google.com/s2/favicons?sz=64&domain=funda.nl
// @grant         GM_xmlhttpRequest
// @connect       www.ep-online.nl
// @connect       www.wozwaardeloket.nl
// ==/UserScript==

//switching stuff for old vs new funda
//https://stackoverflow.com/questions/48587922/using-the-same-userscript-to-run-different-code-at-different-urls

'use strict';

const labelColor = new Map([
  ['A+++', '#00A54E'],
  ['A++', '#4CB948'],
  ['A+', '#BFD72F'],
  ['A', '#FFF100'],
  ['B', '#FDB914'],
  ['C', '#F56E20'],
  ['D', "#EF1C22"],
  ['E', "#EF1C22"],
  ['F', "#EF1C22"],
  ['G', "#EF1C22"],
  [undefined, "#D8A3DD"],
]);


//debugger;

const eponline = 'https://www.ep-online.nl/Energylabel/Search'

async function Request(url, opt = {}) {
  Object.assign(opt, { url, timeout: 5000, responseType: 'json' })
  return new Promise((resolve, reject) => {
    opt.onerror = opt.ontimeout = reject
    opt.onload = resolve
    GM_xmlhttpRequest(opt)
  })
}

function extractPostcode(base) {
  var parts = base.split(' ');
  return parts[0] + parts[1];
}

function extractAddress(base) {
  if (!/\d/.test(base)) return undefined
  //get last number
  var number = base.match('\(\\d\+\)\(\?\!\.\*\\d\)')[0];
  //get last character
  var letter = base.match('\[a\-zA\-Z\]\(\?\!\.\*\[a\-zA\-Z\]\)')[0];
  // if ends with letter, return number+letter, else just number
  return (base.slice(-1) == letter) ? number + ' ' + letter : number
}

async function getToken() {
  let response = await Request(eponline, { method: 'GET' })
  let parser = new DOMParser();
  let responseDoc = parser.parseFromString(response.responseText, "text/html");
  return responseDoc.querySelector('[name="__RequestVerificationToken"]').value;
}

async function getLabel(token, address, postcode) {
  let response = await Request(eponline, {
    method: 'POST',
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    data: new URLSearchParams({
      __RequestVerificationToken: token,
      SearchValue: `${postcode} ${address}`
    })
  })
  return extractLabel(response, address, postcode);
}

function extractLabel(response, address, postcode) {
  //todo: handle multi-page eponline results, one such example is "1072NK 2"
  var parser = new DOMParser();
  var responseDoc = parser.parseFromString(response.responseText, "text/html");

  var labelBlock = Array.from(responseDoc.querySelectorAll('.se-result-item-nta.se-sm-noborder'))
    .filter((doc) => {
      //empty = false, else true
      return doc.querySelector('span.sort-value-pht.text-nowrap').textContent.trim() === postcode + ' ' + address
    })[0]
  return labelBlock
}

function generateLabelSummary(node) {
  if (node === undefined)
    return { text: "issue getting label", label: undefined }

  //get the letter label
  let label = node.querySelector('[class*=bg-label-class-] > span').innerText.trim()

  // check if label is valid
  let Opnamedatum = Array.from(node.querySelectorAll('.se-item-description-nta')).filter(x => x.innerText.trim() === "Opnamedatum")[0].nextElementSibling.innerText.trim()
  if (Opnamedatum === "-") {
    return { text: "unofficial " + label, label: label }
  }

  //check if pre-2021 type label
  let energyIndex = Array.from(node.querySelectorAll('.se-item-description-nta')).filter(x => x.innerText.trim() === "EI")
  if (!!energyIndex.length) {
    return { text: "EnergyIndex: " + energyIndex[0].nextElementSibling.innerText.trim() + " (letter: " + label + ")", label: label }
  }

  // return current energy label class
  return { text: "EnergyLabel: " + label, label: label }

}

async function getWoz() {
  return "WIP"
}

async function generateWozSummary(node) {
  return node
}

function applyEnrichment(nodeToEnrich, labelSummary, wozText) {
  let pLabel = document.createElement('p')
  pLabel.textContent = labelSummary.text
  pLabel.style.backgroundColor = labelColor.get(labelSummary.label)
  nodeToEnrich.after(pLabel)
  // handle pWoz
}

async function enrich(nodes) {
  let token = await getToken()

  await Promise.all(nodes.map(async (node) => {
    let labelNode = await getLabel(token, node.address, node.postcode)
    let labelSummary = generateLabelSummary(labelNode)
    let wozNodes = await getWoz(node.address, node.postcode)
    let wozSummary = generateWozSummary(wozNodes)
    applyEnrichment(node.appendNode, labelSummary, wozSummary)
  }));
}

function getNodesToEnrichPararius() {
  let searchResultBase = document.querySelectorAll('.search-list__item--listing')
  return Array.from(searchResultBase)
    .map(node => {
      let address = extractAddress(node.querySelector('.listing-search-item__link--title').textContent.trim());
      let postcode = extractPostcode(node.querySelector(".listing-search-item__sub-title\\'").textContent.trim());
      let appendNode = node.querySelector('.listing-search-item__features')
      return { appendNode: appendNode, address: address, postcode: postcode };
    })
}

function getNodesToEnrichFunda() {
  let searchResultBase = document.querySelectorAll('[data-test-id="search-result-item"]')
  return Array.from(searchResultBase)
    .map(node => {
      let address = extractAddress(node.querySelector('[data-test-id="street-name-house-number"]').textContent.trim());
      let postcode = extractPostcode(node.querySelector('[data-test-id="postal-code-city"]').textContent.trim());
      let appendNode = node.querySelector('.flex-wrap.overflow-hidden')
      return { appendNode: appendNode, address: address, postcode: postcode };
    })
}

(async () => {

  // https://stackoverflow.com/questions/48587922/using-the-same-userscript-to-run-different-code-at-different-urls
  if (/funda\.nl/.test(location.hostname)) {
    // Run code for new funda.nl
    await enrich(getNodesToEnrichFunda())
  }
  else if (/pararius\.nl/.test(location.hostname)) {
    // Run code for pararius.nl
    await enrich(getNodesToEnrichPararius())
  }
})().catch(err => {
  console.error(err);
});



// process for wozwardeloket:

//await fetch("https://api.pdok.nl/bzk/locatieserver/search/v3_1/suggest?q=2665BH%2C%20105&rows=10", {
//    "credentials": "omit",
//    "headers": {
//        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/114.0",
//        "Accept": "application/json, text/plain, */*",
//        "Accept-Language": "en-US,en;q=0.5",
//        "Sec-Fetch-Dest": "empty",
//        "Sec-Fetch-Mode": "cors",
//        "Sec-Fetch-Site": "cross-site"
//    },
//    "method": "GET",
//    "mode": "cors"
//});

//await fetch("https://api.pdok.nl/bzk/locatieserver/search/v3_1/lookup?fl=*&id=adr-8eba3e1f7fd5d73e3f3402da85f62b7c", {
//    "credentials": "omit",
//    "headers": {
//        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/114.0",
//        "Accept": "application/json, text/plain, */*",
//        "Accept-Language": "en-US,en;q=0.5",
//        "Sec-Fetch-Dest": "empty",
//        "Sec-Fetch-Mode": "cors",
//        "Sec-Fetch-Site": "cross-site"
//    },
//    "method": "GET",
//    "mode": "cors"
//});

//await fetch("https://www.wozwaardeloket.nl/wozwaardeloket-api/v1/wozwaarde/nummeraanduiding/1621200000027796", {
//    "credentials": "include",
//    "headers": {
//        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/114.0",
//        "Accept": "application/json, text/plain, */*",
//        "Accept-Language": "en-US,en;q=0.5",
//        "Sec-Fetch-Dest": "empty",
//        "Sec-Fetch-Mode": "cors",
//        "Sec-Fetch-Site": "same-origin"
//    },
//    "referrer": "https://www.wozwaardeloket.nl/",
//    "method": "GET",
//    "mode": "cors"
//});


//{
//	"properties": {
//		"identificatie": "1621010000027755",
//		"rdf_seealso": "http://bag.basisregistraties.overheid.nl/bag/id/verblijfsobject/1621010000027755",
//		"oppervlakte": 71,
//		"status": "Verblijfsobject in gebruik",
//		"gebruiksdoel": "woonfunctie",
//		"openbare_ruimte": "Dorpsstraat",
//		"huisnummer": 105,
//		"huisletter": "",
//		"toevoeging": "",
//		"postcode": "2665BH",
//		"woonplaats": "Bleiswijk",
//		"bouwjaar": 2017,
//		"pandidentificatie": "1621100000037510",
//		"pandstatus": "Pand in gebruik"
//	}
//}