diff --git a/map.js b/map.js index 824c0036..c98243da 100644 --- a/map.js +++ b/map.js @@ -1,12 +1,13 @@ /*jslint browser: true, for: true, long: true, unordered: true */ /*global window console google municipalities periods */ -/* +/** * Op zoek naar de website? * Bezoek https://basgroot.github.io/bekendmakingen/?in=Hoorn + * + * This function is called by Google Maps API, after loading the library. Function name is sent as query parameter. + * @return {void} */ - -// This function is called by Google Maps API, after loading the library. Function name is sent as query parameter. function initMap() { const appState = { // The map itself: @@ -37,17 +38,22 @@ function initMap() { "infoWindow": null }; + /** + * Customize the map based on query parameters. + * Examples: + * ?in=Hoorn&zoom=15¢er=52.6603118963%2C5.0608995325 + * ?in=Oostzaan + * @return {!Object} + */ function getInitialMapSettings() { - var zoomLevel = appState.initialZoomLevel; - var center = Object.assign({}, municipalities[appState.activeMunicipality].center); - var lat; - var lng; - // ?in=Hoorn&zoom=15¢er=52.6603118963%2C5.0608995325 - // ?in=Oostzaan + let zoomLevel = appState.initialZoomLevel; + let center = Object.assign({}, municipalities[appState.activeMunicipality].center); + let lat; + let lng; if (window.URLSearchParams) { const urlSearchParams = new window.URLSearchParams(window.location.search); - var zoomParam = urlSearchParams.get("zoom"); - var centerParam = urlSearchParams.get("center"); + let zoomParam = urlSearchParams.get("zoom"); + let centerParam = urlSearchParams.get("center"); const municipalityParam = urlSearchParams.get("in"); if (municipalityParam && municipalities[municipalityParam] !== undefined) { appState.activeMunicipality = municipalityParam; @@ -77,9 +83,17 @@ function initMap() { }; } + /** + * Display the popup window. + * https://developers.google.com/maps/documentation/javascript/reference/info-window#InfoWindow.open + * @param {!Object} marker Marker showing the popup. + * @param {string} iconName Icon file name. + * @param {string} header Title. + * @param {string} body Contents. + * @return {void} + */ function showInfoWindow(marker, iconName, header, body) { appState.infoWindow.setContent("

" + header + "

" + body + "

"); - // https://developers.google.com/maps/documentation/javascript/reference/info-window#InfoWindow.open appState.infoWindow.open({ "anchor": marker, "map": appState.map, @@ -87,6 +101,11 @@ function initMap() { }); } + /** + * Parse the response of the license document to find the date the license is granted. + * @param {string} responseXml XML. + * @return {!Array|NodeList} + */ function getAlineas(responseXml) { function replaceTags(value) { @@ -100,7 +119,7 @@ function initMap() { ["", ""], // Hoorn [" :", ":"] // Den Helder https://repository.overheid.nl/frbr/officielepublicaties/gmb/2023/gmb-2023-81009/1/xml/gmb-2023-81009.xml ]; - var result = value; + let result = value; // Remove all double spaces in all forms: Landsmeer https://repository.overheid.nl/frbr/officielepublicaties/gmb/2023/gmb-2023-74508/1/xml/gmb-2023-74508.xml result = result.replace(/\s\s+/g, " "); tags.forEach(function (tag) { @@ -110,8 +129,8 @@ function initMap() { } const parser = new window.DOMParser(); - var xmlDoc; - var zakelijkeMededeling; + let xmlDoc; + let zakelijkeMededeling; try { xmlDoc = parser.parseFromString(replaceTags(responseXml).toLowerCase(), "text/xml"); } catch (e) { @@ -128,12 +147,24 @@ function initMap() { ); } + /** + * Calculate the period since the license was granted, to see if it is applicable for formal objection. + * @param {!Date} date Decision date. + * @return {number} + */ function getDaysPassed(date) { const today = new Date(new Date().toDateString()); // Rounded date const dateFrom = new Date(date.toDateString()); return Math.round((today.getTime() - dateFrom.getTime()) / (1000 * 60 * 60 * 24)); } + /** + * Parse the response of the license document to find the date the license is granted. + * @param {string} responseXml XML response. + * @param {!Object} publication Publication object. + * @param {string} licenseId License ID. + * @return {void} + */ function parseBekendmaking(responseXml, publication, licenseId) { function convertMonthNames(value) { @@ -141,10 +172,10 @@ function initMap() { } function parseDate(value) { - var year = value.substring(6, 10); - var month = value.substring(3, 5); - var day = value.substring(0, 2); - var datumBekendgemaakt; + const year = value.substring(6, 10); + const month = value.substring(3, 5); + const day = value.substring(0, 2); + let datumBekendgemaakt; if (Number.isNaN(parseInt(year, 10)) || Number.isNaN(parseInt(month, 10)) || Number.isNaN(parseInt(day, 10))) { console.error("Error parsing date (" + value + ") of license " + publication.urlApi); return false; @@ -198,11 +229,11 @@ function initMap() { " is een omgevingsvergunning verleend" // Noordoostpolder https://repository.overheid.nl/frbr/officielepublicaties/gmb/2023/gmb-2023-93843/1/xml/gmb-2023-93843.xml ]; const identifierNextValueIsDate = "datum bekendmaking besluit:"; // Den Haag https://repository.overheid.nl/frbr/officielepublicaties/gmb/2023/gmb-2023-79557/1/xml/gmb-2023-79557.xml - var i; - var pos; - var isDateOfDeadline = false; - var isObjectionStartDate = false; - var result; + let i; + let pos; + let isDateOfDeadline = false; + let isObjectionStartDate = false; + let result; if (value === identifierNextValueIsDate) { isNextValueBekendmakingsDate = true; } else { @@ -295,15 +326,15 @@ function initMap() { const alineas = getAlineas(responseXml); const maxLooptijd = (6 * 7) + 1; // 6 weken de tijd om bezwaar te maken const dateFormatOptions = {"weekday": "long", "year": "numeric", "month": "long", "day": "numeric"}; - var datumBekendgemaakt; // Datum verzonden aan belanghebbende(n) - var looptijd; - var resterendAantalDagenBezwaartermijn; - var i; - var j; - var alinea; - var textToShow = ""; - var isNextValueBekendmakingsDate = false; - var isBezwaartermijnFound = false; + let datumBekendgemaakt; // Datum verzonden aan belanghebbende(n) + let looptijd; + let resterendAantalDagenBezwaartermijn; + let i; + let j; + let alinea; + let textToShow = ""; + let isNextValueBekendmakingsDate = false; + let isBezwaartermijnFound = false; for (i = 0; i < alineas.length; i += 1) { alinea = alineas[i]; if (alinea.childNodes.length > 0) { @@ -331,6 +362,12 @@ function initMap() { document.getElementById(licenseId).innerHTML = textToShow; } + /** + * Call the government API for a specific license, to get more details. + * @param {string} licenseId License ID. + * @param {!Object} publication Publication object. + * @return {void} + */ function collectBezwaartermijn(licenseId, publication) { if (publication.urlApi === "UNAVAILABLE") { console.error("Unable to get data for license " + publication.urlDoc); @@ -355,6 +392,13 @@ function initMap() { }); } + /** + * Create the option of a drop down element. + * @param {string} value Key. + * @param {string} displayValue Value. + * @param {boolean} isSelected Selected or not? + * @return {!HTMLOptionElement} + */ function createOption(value, displayValue, isSelected) { const option = document.createElement("option"); option.text = displayValue; @@ -365,12 +409,20 @@ function initMap() { return option; } + /** + * Create the spinner shown when retrieving all licenses. + * @return {void} + */ function createMapsControlLoadingIndicator() { const controlDiv = document.createElement("div"); // Create a DIV to attach the control UI to the Map. controlDiv.appendChild(appState.loadingIndicator); appState.map.controls[google.maps.ControlPosition.RIGHT_CENTER].push(controlDiv); } + /** + * Create the drop down with all municipalities of The Netherlands. + * @return {void} + */ function createMapsControlMunicipalities() { function createOptionEx(value) { @@ -392,10 +444,14 @@ function initMap() { appState.map.controls[google.maps.ControlPosition.TOP_CENTER].push(controlDiv); } + /** + * Create the drop down with time filter. + * @return {void} + */ function createMapsControlPeriods() { function createOptionEx(value, displayValue) { - return createOption(value, displayValue, value === appState.period); + return createOption(value, displayValue, value === "14d"); } const controlDiv = document.createElement("div"); // Create a DIV to attach the control UI to the Map. @@ -410,6 +466,10 @@ function initMap() { appState.map.controls[google.maps.ControlPosition.BOTTOM_CENTER].push(controlDiv); } + /** + * Create the button linking to this source code. + * @return {void} + */ function createMapsControlSource() { const controlDiv = document.createElement("div"); // Create a DIV to attach the control UI to the Map. const button = document.createElement("button"); @@ -425,19 +485,28 @@ function initMap() { appState.map.controls[google.maps.ControlPosition.BOTTOM_LEFT].push(controlDiv); } + /** + * Create the elements on the map. + * https://developers.google.com/maps/documentation/javascript/examples/control-custom + * @return {void} + */ function createMapsControls() { - // https://developers.google.com/maps/documentation/javascript/examples/control-custom createMapsControlLoadingIndicator(); createMapsControlMunicipalities(); createMapsControlPeriods(); createMapsControlSource(); } + /** + * Parse the license ID. + * Options: https://zoek.officielebekendmakingen.nl/prb-2023-962.html + * https://zoek.officielebekendmakingen.nl/gmb-2023-56454.html + * https://zoek.officielebekendmakingen.nl/wsb-2023-801.html + * https://zoek.officielebekendmakingen.nl/stcrt-2023-128.html + * @param {string} websiteUrl Link to document. + * @return {string|boolean} + */ function getLicenseIdFromUrl(websiteUrl) { - // Options: https://zoek.officielebekendmakingen.nl/prb-2023-962.html - // https://zoek.officielebekendmakingen.nl/gmb-2023-56454.html - // https://zoek.officielebekendmakingen.nl/wsb-2023-801.html - // https://zoek.officielebekendmakingen.nl/stcrt-2023-128.html const startOfUrl = "https://zoek.officielebekendmakingen.nl/"; const endOfUrl = ".html"; if (websiteUrl.substring(0, startOfUrl.length) === startOfUrl) { @@ -446,11 +515,17 @@ function initMap() { return false; } + /** + * Choose what image to use for a license type. + * Text mining to get distinguish the different license states and types + * Images are converted to SVG using https://png2svg.com/ + * Resized to 35x45 using https://www.iloveimg.com/resize-image/resize-svg#resize-options,pixels + * Optmized using https://svgoptimizer.com/ + * @param {string} title Name of permit. + * @param {string} type Permit type. + * @return {string} + */ function getIconName(title, type) { - // Text mining to get distinguish the different license states and types - // Images are converted to SVG using https://png2svg.com/ - // Resized to 35x45 using https://www.iloveimg.com/resize-image/resize-svg#resize-options,pixels - // Optmized using https://svgoptimizer.com/ const exploitatievergunningen = [ "drank- en horecavergunning", "exploitatievergunning", @@ -552,12 +627,17 @@ function initMap() { // "verordeningen en reglementen", } + /** + * When two licenses are on the same location, move the second, to see them both. + * @param {!Object} proposedCoordinate Coordinate to place marker. + * @return {!Object} + */ function findUniquePosition(proposedCoordinate) { function isCoordinateAvailable(coordinate) { - var isAvailable = true; // Be positive - var i; - var marker; + let isAvailable = true; // Be positive + let i; + let marker; for (i = 0; i < appState.markersArray.length; i += 1) { // Don't use forEach, to gain some performance. marker = appState.markersArray[i]; @@ -576,6 +656,12 @@ function initMap() { return proposedCoordinate; } + /** + * Set visibility of the markers, based on time filter. + * @param {number} age Days in the past. + * @param {string} periodToShow Selected period. + * @return {boolean} + */ function isMarkerVisible(age, periodToShow) { switch (periodToShow) { case "3d": @@ -589,11 +675,18 @@ function initMap() { } } + /** + * Add a marker to the map. + * @param {!Object} publication Publication object. + * @param {string} periodToShow Selected period. + * @param {!Object} position Coordinate. + * @return {void} + */ function addMarker(publication, periodToShow, position) { function onClick() { const description = publication.description + "

Meer info: " + publication.urlDoc + "."; - var licenseId = getLicenseIdFromUrl(publication.urlDoc); + const licenseId = getLicenseIdFromUrl(publication.urlDoc); // Supported is "Gemeentelijk blad (gmb)", "Provinciaal blad (prb)", "Waterschapsblad (wsb) and Staatscourant (stcrt)" // Options: https://zoek.officielebekendmakingen.nl/prb-2023-962.html // https://zoek.officielebekendmakingen.nl/gmb-2023-56454.html @@ -671,6 +764,15 @@ function initMap() { return markerObject; } + /** + * If visible, create the marker. Otherwise move it to a list and show it when the map is scrolled. + * This reduces load in the browser. + * @param {!Object} publication Publication object. + * @param {string} periodToShow Selected period. + * @param {!Object} position Coordinate. + * @param {!Object} bounds Visible map. + * @return {void} + */ function prepareToAddMarker(publication, periodToShow, position, bounds) { if (bounds.contains(position)) { addMarker(publication, periodToShow, position); @@ -682,6 +784,10 @@ function initMap() { } } + /** + * Determine what time filter must be used. + * @return {!Object} + */ function getPeriodFilter() { function isHistoricalPeriod(value) { @@ -709,20 +815,31 @@ function initMap() { return result; } + /** + * Create (new) latitude/longitude object. + * Input example: "52.35933 4.893097" + * @return {!Object} + */ function createCoordinate(locatiepunt) { - const coordinate = locatiepunt.split(" "); // Example: "52.35933 4.893097" + const coordinate = locatiepunt.split(" "); return { "lat": parseFloat(coordinate[0]), "lng": parseFloat(coordinate[1]) }; } + /** + * Add markers to the map. + * @param {number} startRecord Number of record of current batch. + * @param {boolean} isMoreDataAvailable More to load? + * @return {void} + */ function addMarkers(startRecord, isMoreDataAvailable) { const periodFilter = getPeriodFilter(); const bounds = appState.map.getBounds(); - var position; - var i; - var publication; + let position; + let i; + let publication; console.log("Adding markers " + startRecord + " to " + appState.publicationsArray.length); for (i = startRecord - 1; i < appState.publicationsArray.length; i += 1) { publication = appState.publicationsArray[i]; @@ -752,11 +869,15 @@ function initMap() { } } + /** + * Add municipality markers to the map. + * @return {void} + */ function addMunicipalitiyMarkers() { const municipalityNames = Object.keys(municipalities); municipalityNames.forEach(function (municipalityName) { const municipalityObject = municipalities[municipalityName]; - var marker = new google.maps.Marker({ + let marker = new google.maps.Marker({ "map": appState.map, "position": municipalityObject.center, "label": municipalityName, // https://developers.google.com/maps/documentation/javascript/reference/marker#MarkerLabel @@ -788,6 +909,10 @@ function initMap() { }); } + /** + * Reset time filter. + * @return {void} + */ function updatePeriodFilter() { const periodFilter = getPeriodFilter(); if (periodFilter.isHistory) { @@ -813,8 +938,11 @@ function initMap() { } } - /* - * Add municipality and other parameters to the URL, so the view can be shared. + /** + * Add municipality and other parameters to the URL, so the view can be shared. + * @param {number|string} zoom Zoom level. + * @param {!Object} center Coordinate of center of map. + * @return {void} */ function updateUrl(zoom, center) { // Add to URL: /?in=Alkmaar&zoom=15¢er=52.43660651356703,4.84418395002761 @@ -828,8 +956,11 @@ function initMap() { document.title = "Bekendmakingen " + appState.activeMunicipality; } - /* - * Calculate the distance between two points, using the haversine formula. + /** + * Calculate the distance between two points, using the haversine formula. + * @param {!Object} from Coordinate 1. + * @param {!Object} to Coordinate 2. + * @return {number} */ function computeDistanceBetween(from, to) { // Source: http://www.movable-type.co.uk/scripts/latlong.html @@ -845,12 +976,14 @@ function initMap() { return radius * c; // Distance in metres } - /* - * Not accurate, but try to find the closest municipality center. + /** + * Not accurate, but try to find the closest municipality center. + * @param {!Object} position Coordinate to start. + * @return {void} */ function activateClosestMunicipality(position) { const municipalityNames = Object.keys(municipalities); - var distance = 1000000; + let distance = 1000000; municipalityNames.forEach(function (municipalityName) { const municipalityObject = municipalities[municipalityName]; const distanceBetweenMunicipalityAndViewer = computeDistanceBetween(position, municipalityObject.center) / 1000; @@ -862,8 +995,9 @@ function initMap() { }); } - /* - * Determine if the municipality is part of the URL. + /** + * Determine if the municipality is part of the URL. + * @return {boolean} */ function isLocationInUrl() { if (window.URLSearchParams) { @@ -876,8 +1010,9 @@ function initMap() { return false; } - /* - * Try to find the municipality of the visitor, by using an IP geolocation API. + /** + * Try to find the municipality of the visitor, by using an IP geolocation API. + * @return {void} */ function getLocationByIp() { fetch( @@ -911,8 +1046,9 @@ function initMap() { }); } - /* - * Try to find the municipality of the visitor, by using the device position, or IP geolocation API. + /** + * Try to find the municipality of the visitor, by using the device position, or IP geolocation API. + * @return {void} */ function getLocationAndLoadData() { @@ -944,6 +1080,10 @@ function initMap() { navigator.geolocation.getCurrentPosition(deviceLocationFound, deviceLocationRequestRejected); } + /** + * Setup map. + * @return {void} + */ function internalInitMap() { const containerElm = document.getElementById("map"); const mapSettings = getInitialMapSettings(); @@ -995,8 +1135,8 @@ function initMap() { // Time to display other markers.. const bounds = appState.map.getBounds(); const periodFilter = getPeriodFilter(); - var delayedMarker; - var i = appState.delayedMarkersArray.length; + let delayedMarker; + let i = appState.delayedMarkersArray.length; while (i > 0) { i = i - 1; delayedMarker = appState.delayedMarkersArray[i]; @@ -1011,14 +1151,24 @@ function initMap() { loadData(true); } + /** + * Scroll map to a certain coordinate (center of municipality). + * @param {string} municipality Municipality to center. + * @return {void} + */ function navigateTo(municipality) { const center = municipalities[municipality].center; appState.map.setCenter(new google.maps.LatLng(center.lat, center.lng), appState.initialZoomLevel); } + /** + * Convert license URL to API endpoint. + * URL: https://zoek.officielebekendmakingen.nl/gmb-2022-425209.html + * Endpoint: https://repository.overheid.nl/frbr/officielepublicaties/gmb/2022/gmb-2022-425209/1/xml/gmb-2022-425209.xml + * @param {string} urlDoc URL of publication. + * @return {string} + */ function getUrlApi(urlDoc) { - // URL: https://zoek.officielebekendmakingen.nl/gmb-2022-425209.html - // Endpoint: https://repository.overheid.nl/frbr/officielepublicaties/gmb/2022/gmb-2022-425209/1/xml/gmb-2022-425209.xml const licenseId = getLicenseIdFromUrl(urlDoc); if (!licenseId) { return "UNAVAILABLE"; @@ -1031,6 +1181,11 @@ function initMap() { return "https://repository.overheid.nl/frbr/officielepublicaties/" + licenseIdArray[0] + "/" + licenseIdArray[1] + "/" + licenseId + "/1/xml/" + licenseId + ".xml"; } + /** + * Parse API response. + * @param {!Object} responseJson JSON response. + * @return {void} + */ function addPublications(responseJson) { responseJson.searchRetrieveResponse.records.record.forEach(function (inputRecord) { const urlDoc = inputRecord.recordData.gzd.originalData.meta.tpmeta.bronIdentifier.trim(); @@ -1070,6 +1225,10 @@ function initMap() { }); } + /** + * Hide the active municipality marker, since this overlaps the licenses. + * @return {void} + */ function hideActiveMunicipalityMarker() { appState.municipalityMarkers.forEach(function (markerObject) { if (markerObject.municipalityName === appState.activeMunicipality) { @@ -1078,6 +1237,11 @@ function initMap() { }); } + /** + * Open an historical file, where data is stored per month. + * @param {string} period Month to display. + * @return {void} + */ function loadHistory(period) { const lookupMunicipality = ( municipalities[appState.activeMunicipality].hasOwnProperty("lookupName") @@ -1120,6 +1284,12 @@ function initMap() { }); } + /** + * Call the API to get live data. + * @param {string} municipality Municipality to load. + * @param {number} startRecord Start of batch. + * @return {void} + */ function loadDataForMunicipality(municipality, startRecord) { const lookupMunicipality = ( municipalities[municipality].hasOwnProperty("lookupName") @@ -1136,7 +1306,7 @@ function initMap() { ).then(function (response) { if (response.ok) { response.json().then(function (responseJson) { - var isMoreDataAvailable; + let isMoreDataAvailable; if (municipality !== appState.activeMunicipality || appState.isHistoryActive) { // We are loading a different municipality, but user selected another one. return; @@ -1165,6 +1335,11 @@ function initMap() { }); } + /** + * Clear markers, because of a different period, or municipality. + * @param {string} municipalityToHide Municipality to show licenses from. + * @return {void} + */ function clearMarkers(municipalityToHide) { // https://developers.google.com/maps/documentation/javascript/markers#remove appState.markersArray.forEach(function (markerObject) { @@ -1177,6 +1352,11 @@ function initMap() { appState.delayedMarkersArray = []; } + /** + * Populate the map with markers. + * @param {boolean} isNavigationNeeded Move to different center. + * @return {void} + */ function loadData(isNavigationNeeded) { const municipalityComboElm = document.getElementById("idCbxMunicipality"); const periodFilter = getPeriodFilter();