import * as L from 'leaflet-contextmenu'; import { Feature, FeatureCollection, Point } from 'geojson'; import * as MessagePack from "@msgpack/msgpack"; interface PlaceModel { id: number | null; name: string; address: string; open_hours: string; icon: string; description: string; latitude: number; longitude: number; url: string | null; } async function loadPlaces(): Promise> { let bytes = await fetch('places').then(response => response.body); return (await MessagePack.decodeAsync(bytes)) as Array; } function toFeature(place: PlaceModel): Feature { return { "type": "Feature", "properties": { "name": place.name, "address": place.address, "open_hours": place.open_hours, "icon": place.icon, "description": place.description, "url": place.url, }, "geometry": { "type": "Point", "coordinates": [place.longitude, place.latitude] } } } function toLeafletPlaces(backendPlaces: Array): L.GeoJSON { let result: FeatureCollection = { type: "FeatureCollection", features: new Array(), } for (const place of backendPlaces) { result.features.push(toFeature(place)); } return result; } let placesLayer: L.GeoJSON; let places = new Map(); function toStr(latlng: L.LatLngLiteral) { return latlng.lng + "|" + latlng.lat; } function getPlaceFromForm(): PlaceModel { const idStr = (document.getElementById("id") as HTMLInputElement).value; const id = idStr == "" ? null : parseInt(idStr); const name = (document.getElementById("name") as HTMLInputElement).value; const address = (document.getElementById("address") as HTMLInputElement).value; const open_hours = (document.getElementById("open_hours") as HTMLTextAreaElement).value; const icon = (document.getElementById("icon") as HTMLSelectElement).value; const description = (document.getElementById("description") as HTMLTextAreaElement).value; const latitude = parseFloat( (document.getElementById("lat") as HTMLInputElement).value); const longitude = parseFloat( (document.getElementById("long") as HTMLSelectElement).value); const url = (document.getElementById("url") as HTMLInputElement).value; return { id: id, name: name, address: address, open_hours: open_hours, icon: icon, description: description, latitude: latitude, longitude: longitude, url: url, }; } function clearForm(): void { /* Get the form elements*/ const idInput = (document.getElementById("id") as HTMLInputElement); const latInput = (document.getElementById("lat") as HTMLInputElement); const longInput = (document.getElementById("long") as HTMLInputElement); const nameInput = (document.getElementById("name") as HTMLInputElement); const addressInput = (document.getElementById("address") as HTMLInputElement); const urlInput = (document.getElementById("url") as HTMLInputElement); const openHoursArea = (document.getElementById("open_hours") as HTMLTextAreaElement); const descriptionArea = (document.getElementById("description") as HTMLTextAreaElement); /* Clear them */ idInput.value = ""; latInput.value = ""; longInput.value = ""; nameInput.value = ""; addressInput.value = ""; urlInput.value = ""; openHoursArea.value = ""; descriptionArea.value = ""; /* Clear the callback */ document.getElementById("button").onclick = null; /* Now you see it, now you don't*/ document.getElementById("modal").style.display = "none"; } async function createPlace(): Promise { const newPlace = getPlaceFromForm(); await fetch('places', { method: 'PUT', headers: { 'Content-Type': 'application/msgpack', }, body: MessagePack.encode(newPlace), }) .then((response) => response.body) .then((bytes) => MessagePack.decodeAsync(bytes)) .then((place: PlaceModel) => { places.set( toStr({ lat: place.latitude, lng: place.longitude }), place ); placesLayer.addData(toFeature(place)); clearForm(); alert("Lugar añadido exitosamente"); }) .catch((error) => alert(error)); } async function editPlace(): Promise { const newPlace = getPlaceFromForm(); await fetch('places', { method: 'PUT', headers: { 'Content-Type': 'application/msgpack', }, body: MessagePack.encode(newPlace), }) .then((response) => response.body) .then((bytes) => MessagePack.decodeAsync(bytes)) .then((place: PlaceModel) => { places.set( toStr({ lat: place.latitude, lng: place.longitude }), place ); clearForm(); alert("Lugar guardado exitosamente. Para ver los cambios, recargue la página."); }) .catch((error) => alert(error)); } const enum Operation { Create = "create", Edit = "edit", } interface MapEvent { latlng: L.LatLng; relatedTarget: L.Marker | L.Path | undefined; } async function getAddressReverse(lat: number, long: number): Promise { const nominatimEndpoint = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${long}&format=json` const address = (await fetch(nominatimEndpoint).then(response => response.json())).address; return `${address.road} ${address.house_number}, ${address.city}`; } function toLink(url: string): string { let content = url; const m = url.match("https://instagram.com/(.*)"); if (m) { content = `@${m[1]}`; } return `${content}` } async function setupMap(): Promise { /* Create/Edit form */ const modal = document.getElementById("modal"); const closeButton = document.getElementById("close"); closeButton.onclick = function() { modal.style.display = "none"; } window.onclick = function(e: Event) { if (e.target == modal) { modal.style.display = "none"; } } async function openForm(op: Operation, lat: number, long: number): Promise { /* Fill the form for us */ const h1 = modal.getElementsByTagName("h1")[0]; if (op == Operation.Create) { clearForm() h1.innerText = "Añadir lugar nuevo"; } else { h1.innerText = "Editar lugar"; } const latInput = (document.getElementById("lat") as HTMLInputElement); const longInput = (document.getElementById("long") as HTMLInputElement); latInput.value = lat.toString(); longInput.value = long.toString(); const addressInput = (document.getElementById("address") as HTMLInputElement); if (op == Operation.Edit) { const place = places.get(toStr({ lat: lat, lng: long })); console.log(toStr({ lat: lat, lng: long })); /*Get the form elements*/ const idInput = (document.getElementById("id") as HTMLInputElement); const nameInput = (document.getElementById("name") as HTMLInputElement); const urlInput = (document.getElementById("url") as HTMLInputElement); const openHoursArea = (document.getElementById("open_hours") as HTMLTextAreaElement); const iconSelect = (document.getElementById("icon") as HTMLSelectElement); const descriptionArea = (document.getElementById("description") as HTMLTextAreaElement); /* And set them */ idInput.value = place.id.toString(); nameInput.value = place.name; addressInput.value = place.address; urlInput.value = place.url; openHoursArea.value = place.open_hours; iconSelect.value = place.icon; descriptionArea.value = place.description; } else { getAddressReverse(lat, long).then( (address) => { if (addressInput.value.length == 0) { addressInput.value = address; } } ); } /* Plug callbacks */ if (op == Operation.Create) { document.getElementById("button").onclick = createPlace; } else { document.getElementById("button").onclick = editPlace; } /* Make it appear */ modal.style.display = "block"; } function openCreateForm(e: MapEvent) { const lat = e.latlng.lat; const long = e.latlng.lng; openForm(Operation.Create, lat, long); } function openEditForm(e: MapEvent) { const marker = (e.relatedTarget as L.Marker); const lat = marker.getLatLng().lat; const long = marker.getLatLng().lng; openForm(Operation.Edit, lat, long); } /* Get places from backend */ const backendPlaces = await loadPlaces(); const leafletPlaces = toLeafletPlaces(backendPlaces); for (const place of backendPlaces) { places.set( toStr({ lat: place.latitude, lng: place.longitude }), place ); } /* Set up the map*/ const map = new L.map('map', { contextmenu: true, contextmenuWidth: 140, contextmenuItems: [ { text: 'Añadir lugar', callback: openCreateForm } ] }); /* Create the tile layer with correct attribution*/ const osmUrl = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'; const osmAttrib = 'Mapa © OpenStreetMap'; const osm = new L.TileLayer(osmUrl, { minZoom: 4, maxZoom: 20, attribution: osmAttrib }); /* Start the map in Santiago */ map.setView(new L.LatLng(-33.45, -70.666667), 13); /* Try to get user position, if not, put the map in Santiago again */ map.locate({ setView: true, maxZoom: 16 }) .on('locationerror', function(_event: L.LocationEvent) { map.setView(new L.LatLng(-33.45, -70.666667), 13); }); map.addLayer(osm); function onEachFeature(feature: Feature, layer: L.Layer) { if (feature.properties) { let popupStr = `

${feature.properties.name}

`; popupStr += "
    " if (feature.properties.address) popupStr += `
  • Dirección:${feature.properties.address}
  • `; if (feature.properties.open_hours) popupStr += `
  • Horario:${feature.properties.open_hours}
  • `; if (feature.properties.description) popupStr += `
  • ${feature.properties.description}
  • `; if (feature.properties.url) popupStr += `
  • ${toLink(feature.properties.url)}
  • `; const lnglat = (feature.geometry as Point).coordinates; const lng = lnglat[0]; const lat = lnglat[1]; popupStr += `GMaps` popupStr += "
"; layer.bindPopup(popupStr); layer.bindContextMenu({ contextmenu: true, contextmenuInheritItems: false, contextmenuItems: [{ text: 'Editar', callback: openEditForm, }] }); } } /* Icons */ const icons = new Map(); icons.set('bar', new L.Icon({ iconUrl: 'icons/bar.svg' })); icons.set('coffee', new L.Icon({ iconUrl: 'icons/coffee.svg' })); icons.set('cinema', new L.Icon({ iconUrl: 'icons/film.svg' })); icons.set('dining', new L.Icon({ iconUrl: 'icons/dining.svg' })); icons.set('food', new L.Icon({ iconUrl: 'icons/food.svg' })); icons.set('jazz', new L.Icon({ iconUrl: 'icons/saxophone.svg' })); icons.set('library', new L.Icon({ iconUrl: 'icons/book.svg' })); icons.set('marker', new L.Icon({ iconUrl: 'icons/marker.svg' })); icons.set('mask', new L.Icon({ iconUrl: 'icons/mask.svg' })); icons.set('museum', new L.Icon({ iconUrl: 'icons/museum.svg' })); icons.set('shop', new L.Icon({ iconUrl: 'icons/store.svg' })); for (let [_name, icon] of icons) { icon.options.iconSize = [36, 36]; icon.options.popupAnchor = [0, -18]; } function pointToLayer(feature: Feature, latlng: L.LatLng) { let markerIcon = null; if (feature.properties && feature.properties.icon) { markerIcon = icons.get(feature.properties.icon); } if (markerIcon !== null && markerIcon !== undefined) return L.marker(latlng, { icon: markerIcon }); else return L.marker(latlng); } placesLayer = L.geoJSON(leafletPlaces, { onEachFeature: onEachFeature, pointToLayer: pointToLayer }) map.addLayer(placesLayer); }