'use strict'; import UI from "./ui.js"; import API_CONFIG from "./config.js"; import Icons from "./icons.js"; import { SphericalPhoto } from "./components/SphericalPhoto.js"; const MAPBOX_TOKEN = 'pk.eyJ1Ijoibmljb3BhIiwiYSI6ImNseWNwZjJjbjFidzcya3BoYTU0bHg4NnkifQ.3036JnCXZTEMt6jVgMzVRw'; const BASE_URL = location.href; let API_URL = ''; if (BASE_URL.includes('localhost')) { API_URL = API_CONFIG.dev; } else { API_URL = API_CONFIG.prod; } window.API_URL = API_URL; // Global leaflet /** * @namespace GIS */ const GIS = {}; GIS.CENTER_COORDS = [40.5492, 14.2317]; GIS.INIT_ZOOM = 14; GIS.MIN_ZOOM = 11; GIS.MAX_ZOOM = 24; const optionsVincoli = { color: '#222', opacity: 0.8, weight: 1, fillColor: '#fa7861', fillOpacity: 0.8 }; const optionsSiti = { color: '#800040', opacity: 1, weight: 1.5, fillColor: '#800040', fillOpacity: 0.8 }; const optionsGrotta = { color: '#205dac', opacity: 1, weight: 1.5, fillColor: '#205dac', fillOpacity: 0.8 } const optionsPaesistici = { color: '#222', opacity: 1, weight: 1.5, fillColor: '#88d28d', fillOpacity: 0.8 }; const optionsFabbricati = { color: '#222', opacity: 1, weight: 1.5, fillColor: '#5b5d5f', fillOpacity: 0.8 }; const clusterOptions = { spiderfyOnMaxZoom: false, showCoverageOnHover: false, disableClusteringAtZoom: 19, }; /** * Capitalize a text string * @todo Move to utils * @param {?string} text * @returns {?string} The capitalized string or null */ function capitalize(text) { let capital = text; if (text) { let words = text.split(' '); capital = ''; for (let w of words) { w = w[0].toUpperCase() + w.slice(1); capital += w + ' '; } capital.trimEnd(); } return capital; } /** * @param {string} mapId * @param {number} zoomLevel * @returns {Map} */ GIS.initMap = async function (mapId, zoomLevel = this.INIT_ZOOM) { let map = L.map(mapId, { //attributionControl: false, minZoom: GIS.MIN_ZOOM, }).setView(this.CENTER_COORDS, zoomLevel); map.crs = L.CRS.EPSG4326; const baseMap = await this.initLayers(map); // Add scale and ruler controls L.control.scale({imperial: false}).addTo(map); L.control.graphicScale({fill: 'hollow', position: 'bottomright'}).addTo(map); let layerVincoli = await this.loadGeoJSON('vincoli.geojson', optionsVincoli); let layerPaesistici = await this.loadGeoJSON('paesistici.geojson', optionsPaesistici); // Refactor with separate function... this.sites().then(data => { data.markers.addTo(map); data.geom.addTo(map); window.Sites = data.markers; }); this.notConserved().then(group => {group.addTo(map); window.NotConserved = group}); this.findings().then(group => {group.addTo(map); window.Findings = group}); this.prehistoric().then(group => {group.addTo(map); window.Prehistoric = group}); const archeo = { 'Vincoli archeologici' : layerVincoli, 'Vincoli paesistici' : layerPaesistici, }; L.control.layers(baseMap, archeo).addTo(map); return map; } /** * Create markers for sites * @returns {{markers: L.markerClusterGroup, geom: L.LayerGroup}} */ GIS.sites = async function () { let sitesData = await fetch(`${API_URL}/sites`) .then(data => data.json()); let sites = L.markerClusterGroup(clusterOptions); let geom = []; for (let record of sitesData) { if (record.geojson) { const options = record.gisId === 'grotta_azzurra' ? optionsGrotta : optionsSiti; geom.push(await this.loadSiteLayer(record, options)); } sites.addLayer(L.marker( record.coordinates, {icon: Icons.site} ).bindTooltip(record.label + '
(Clicca per aprire scheda)') .on( 'click', () => UI.openSiteModal(record, '#site-data') ) ); } return {markers: sites, geom: L.layerGroup(geom)}; } /** * Create not conserved group * @returns {L.MarkerClusterGroup} */ GIS.notConserved = async function () { let notConserData = await fetch(`${API_URL}/not_conserved`) .then(data => data.json()); let notConserved = L.markerClusterGroup( clusterOptions ); for (let record of notConserData.records) { notConserved.addLayer(L.marker( record.coordinates, {icon: Icons.notConserved} ).bindTooltip(record.denomination) .on( 'click', () => UI.openNotConserModal(record, '#not-conser-data') ) ); } return notConserved; } /** * Create findings group * @returns {L.MarkerClusterGroup} */ GIS.findings = async function () { let findingsData = await fetch(`${API_URL}/finding`) .then(data => data.json()); let findings = L.markerClusterGroup( clusterOptions ); for (let record of findingsData) { findings.addLayer(L.marker( record.coordinates, {icon: Icons.finding} ).bindTooltip(record.object) .on( 'click', () => UI.openFindingModal(record, '#finding-data') ) ); } return findings; } /** * Create group for prehistoric sites * */ GIS.prehistoric = async function () { let data = await fetch(`${API_URL}/prehistoric`) .then(data => data.json()); let prehistoric = L.markerClusterGroup( clusterOptions ); for (let record of data.records) { prehistoric.addLayer(L.marker( record.coordinates, {icon: Icons.prehistoric} ).bindTooltip(record.denomination) .on( 'click', () => UI.openPrehistModal(record, '#prehist-data') ) ); } return prehistoric; } /** * Adds layers to map and returns an object * with {baseMap, archeoLayers, sitesLayerGroup} * @todo Load areas for sites that have them!! * @param {L.Map} map * @returns {{baseMap: {"OpenStreetMap": L.TileLayer}}} */ GIS.initLayers = async function(map) { let osmap = new L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { maxNativeZoom : GIS.MAX_ZOOM, maxZoom: GIS.MAX_ZOOM, attribution: '© OpenStreetMap contributors' }); let mapbox = new L.tileLayer(`https://api.mapbox.com/v4/{id}/{z}/{x}/{y}@2x.jpg90?access_token=${MAPBOX_TOKEN}`, { id : 'mapbox.satellite', tileSize : 512, zoomOffset : -1, maxNativeZoom : GIS.MAX_ZOOM, maxZoom: GIS.MAX_ZOOM, attribution: '© Mapbox' }); let boundaries = await this.loadGeoJSON('confini.geojson', {}, false); let buildings = await this.loadGeoJSON('fabbricati.geojson', optionsFabbricati, false); let baseCatasto = new L.LayerGroup([buildings, boundaries]); const baseGroup = new L.LayerGroup([osmap]); baseGroup.addTo(map); const baseMap = { "OpenStreetMap" : osmap, "Satellite" : mapbox, "Cartografia catastale" : baseCatasto, }; return baseMap; } /** * Toggle spherical photos on the map * based on zoom level */ GIS.toggleSpherical = async function(map) { // TODO Create points layer const spherical = await this._fetchData('spherical'); let sphereMarkers = []; const sPhoto = new SphericalPhoto(); if (spherical) { for (let pano of spherical) { let marker = L.marker(pano.coordinates, { icon: Icons.camera, }); marker.bindTooltip('Apri foto sferica'); marker.on('click', () => { let modal = document.querySelector('#spherical-modal'); let viewer = document.querySelector('#pano-viewer'); viewer.innerHTML = ''; modal.classList.add('is-active'); modal.querySelector('.modal-background').addEventListener('click', () => { modal.classList.remove('is-active'); }); modal.querySelector('.modal-close').addEventListener('click', () => { modal.classList.remove('is-active'); }) sPhoto.setViewer(pano.filename); }); sphereMarkers.push(marker); } } let group = L.layerGroup(sphereMarkers); map.on('zoomend', () => { if (map.getZoom() >= 19) { group.addTo(map); } else { group.remove(); } }); } /** * @param {string} geoJSON geoJSON filename * @param {{color, opacity, weight, fillColor, fillOpacity}} options Style options for features * @param {boolean} popup Should the features have a popup? * @returns {L.Layer} */ GIS.loadGeoJSON = async function (geoJSON, options, popup = true) { const geo = await fetch(`${BASE_URL}/geojson/${geoJSON}`) .then(res => res.json()) .catch(error => console.error(`Can't load layer ${geoJSON}. Reason: ${error}`)); // Show data from feature in popUp? const layer = new L.geoJson(geo, { style: function () { return options; }, onEachFeature: function (feature, layer) { if (popup) { layer.bindPopup(GIS.featurePopup(geoJSON, feature)); } } }); return layer; } /** * @param {object} site Site record from DB * @param {{color, opacity, weight, fillColor, fillOpacity}} options Style options for features * @param {boolean} popup Should the features have a popup? * @returns {L.Layer} */ GIS.loadSiteLayer = async function (site, options, popup = true) { const geoJSON = `${site.gisId}.geojson`; const geo = await fetch(`${BASE_URL}/geojson/${geoJSON}`) .then(res => res.json()) .catch(error => console.error(`Can't load layer ${geoJSON}. Reason: ${error}`)); this.cacheDBData(site.gisId, site.id); // Show data from feature in popUp? const layer = new L.geoJson(geo, { style: function () { return options; }, onEachFeature: function (feature, layer) { if (popup) { layer.bindPopup(GIS.featurePopup(geoJSON, feature)); } else { layer.on("click", async () => { let data = GIS.layerData(site.gisId, site.id); if (typeof data === 'object') { UI.openSiteModal(data); } }); } } }); return layer; } /** * Retrieves data for a given layer * @param {string} layerId * @param {string} dbId * @returns {object} Data for this layer from DB or cache */ GIS.layerData = async function (layerId, dbId) { const fromStorage = localStorage.getItem(layerId); let data = {}; if (fromStorage !== 'undefined') { try { data = JSON.parse(fromStorage); } catch (error) { console.log(error); } } else { data = await GIS._fetchData('sites/' + dbId); } return data; } /** * Get spherical photos for a site * @param {int} siteId */ GIS.getSpherical = async function (siteId) { const data = await this._fetchData('spherical/' + siteId); } /** * Cache data from DB in local storage * for a given layer * @param {string} layerId */ GIS.cacheDBData = async function (layerId, dbId) { const data = await this._fetchData('sites/' + dbId); localStorage.setItem(layerId, JSON.stringify(data)); } /** * Generate proper content for features popup * @todo Hard-coded names!! * * @param {string} layerName * @param {object} feature * @returns {string} The popup's content */ GIS.featurePopup = function (layerName, feature) { const html = `
Oggetto${feature.properties.OGGETTO}
Anno${feature.properties.ANNO}
Comune${capitalize(feature.properties.COMUNE)}
Località${capitalize(feature.properties.LOCALITA)}
Proprietà${capitalize(feature.properties.PROPRIETA)}
`; const content = { 'vincoli.geojson' : html, 'paesistici.geojson' : html, }; return content[layerName]; } /** * Fetch data from API * @param {string} recordId */ GIS._fetchData = async function (recordId) { const data = await fetch(`${API_URL}/${recordId}`) .then(res => res.json()) .catch(err => console.log('Error fetching data from DB: ' + err)); return data; } export default GIS;