From b00e4b1aabf10de64bff7a06b386a94af3429aa4 Mon Sep 17 00:00:00 2001 From: Felipe Date: Tue, 22 Nov 2022 22:03:16 -0300 Subject: [PATCH] feat: Add and Edit places through Leaflet.contextmenu (#4) --- Cargo.lock | 2 +- build.rs | 5 + migrations/20221121211314_swap_lat_long.sql | 1 + src/routes.rs | 7 +- static/index.html | 119 ++++++++- ts-client/Makefile | 3 + ts-client/client.ts | 264 ++++++++++++++++++-- ts-client/package-lock.json | 13 +- ts-client/package.json | 3 +- 9 files changed, 385 insertions(+), 32 deletions(-) create mode 100644 build.rs create mode 100644 migrations/20221121211314_swap_lat_long.sql create mode 100644 ts-client/Makefile diff --git a/Cargo.lock b/Cargo.lock index f959e15..3937e76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -654,7 +654,7 @@ checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" [[package]] name = "huellas" -version = "0.1.0" +version = "0.1.1" dependencies = [ "rocket", "rocket_db_pools", diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..7609593 --- /dev/null +++ b/build.rs @@ -0,0 +1,5 @@ +// generated by `sqlx migrate build-script` +fn main() { + // trigger recompilation when a new migration is added + println!("cargo:rerun-if-changed=migrations"); +} \ No newline at end of file diff --git a/migrations/20221121211314_swap_lat_long.sql b/migrations/20221121211314_swap_lat_long.sql new file mode 100644 index 0000000..ca86355 --- /dev/null +++ b/migrations/20221121211314_swap_lat_long.sql @@ -0,0 +1 @@ +UPDATE places SET longitude=latitude, latitude=longitude; diff --git a/src/routes.rs b/src/routes.rs index d278712..679540d 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -43,7 +43,7 @@ enum UpsertResponse { NotFound(NotFound>), } -#[post("/places", format = "json", data = "")] +#[put("/places", format = "json", data = "")] async fn upsert_place(db: Connection, place: Json) -> Result { if place.id.is_some() { update_place(db, place).await @@ -79,14 +79,15 @@ async fn insert_place(mut db: Connection, mut place: Json) -> Result< async fn update_place(mut db: Connection, place: Json) -> Result { let result = ::sqlx::query!( - "UPDATE places SET (name, address, open_hours, icon, description, longitude, latitude) = (?, ?, ?, ?, ?, ?, ?)", + "UPDATE places SET (name, address, open_hours, icon, description, longitude, latitude) = (?, ?, ?, ?, ?, ?, ?) WHERE id = ?", place.name, place.address, place.open_hours, place.icon, place.description, place.longitude, - place.latitude + place.latitude, + place.id, ) .execute(&mut *db) .await?; diff --git a/static/index.html b/static/index.html index 9d957b6..2bf701f 100644 --- a/static/index.html +++ b/static/index.html @@ -4,12 +4,18 @@ 👣 Huellas 🐾 - - +
+ diff --git a/ts-client/Makefile b/ts-client/Makefile new file mode 100644 index 0000000..e33a104 --- /dev/null +++ b/ts-client/Makefile @@ -0,0 +1,3 @@ +all: client.ts + tsc + sed -i '1d' build/client.js diff --git a/ts-client/client.ts b/ts-client/client.ts index c2fce1a..a118efc 100644 --- a/ts-client/client.ts +++ b/ts-client/client.ts @@ -1,7 +1,8 @@ -import * as L from 'leaflet'; -import { Feature, FeatureCollection, GeoJSON } from 'geojson'; +import * as L from 'leaflet-contextmenu'; +import { Feature, FeatureCollection } from 'geojson'; interface PlaceModel { + id: number | null; name: string; address: string; open_hours: string; @@ -9,43 +10,253 @@ interface PlaceModel { description: string; latitude: number; longitude: number; - } async function loadPlaces(): Promise> { - return await fetch('places/').then(response => response.json()); + return await fetch('places').then(response => response.json()); } -function toLeafletPlaces(backendPlaces: Array): GeoJSON { +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 + }, + "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({ - "type": "Feature", - "properties": { - "name": place.name, - "address": place.address, - "open_hours": place.open_hours, - "icon": place.icon, - "description": place.description - }, - "geometry": { - "type": "Point", - "coordinates": [place.latitude, place.longitude] - } - }); + 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); + + return { + id: id, + name: name, + address: address, + open_hours: open_hours, + icon: icon, + description: description, + latitude: latitude, + longitude: longitude, + }; +} + +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 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 = ""; + 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/json', + }, + body: JSON.stringify(newPlace), + }) + .then((response) => response.json()) + .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/json', + }, + body: JSON.stringify(newPlace), + }) + .then((response) => response.json()) + .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 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"; + } + } + + function openForm(op: Operation, lat: number, long: number): void { + /* 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(); + + 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 addressInput = (document.getElementById("address") 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; + openHoursArea.value = place.open_hours; + iconSelect.value = place.icon; + descriptionArea.value = place.description; + } + + /* 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'); + 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'; @@ -74,6 +285,14 @@ async function setupMap(): Promise { popupStr += ""; layer.bindPopup(popupStr); + layer.bindContextMenu({ + contextmenu: true, + contextmenuInheritItems: false, + contextmenuItems: [{ + text: 'Editar', + callback: openEditForm, + }] + }); } } @@ -107,8 +326,9 @@ async function setupMap(): Promise { return L.marker(latlng); } - map.addLayer(L.geoJSON(leafletPlaces, { + placesLayer = L.geoJSON(leafletPlaces, { onEachFeature: onEachFeature, pointToLayer: pointToLayer - })); + }) + map.addLayer(placesLayer); } diff --git a/ts-client/package-lock.json b/ts-client/package-lock.json index f672795..0ca93d0 100644 --- a/ts-client/package-lock.json +++ b/ts-client/package-lock.json @@ -6,7 +6,8 @@ "": { "dependencies": { "geojson": "^0.5.0", - "leaflet": "^1.8.0" + "leaflet": "^1.8.0", + "leaflet-contextmenu": "^1.4.0" }, "devDependencies": { "@types/leaflet": "^1.7.11" @@ -39,6 +40,11 @@ "version": "1.8.0", "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.8.0.tgz", "integrity": "sha512-gwhMjFCQiYs3x/Sf+d49f10ERXaEFCPr+nVTryhAW8DWbMGqJqt9G4XuIaHmFW08zYvhgdzqXGr8AlW8v8dQkA==" + }, + "node_modules/leaflet-contextmenu": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/leaflet-contextmenu/-/leaflet-contextmenu-1.4.0.tgz", + "integrity": "sha512-BXASCmJ5bLkuJGDCpWmvGqhZi5AzeOY0IbQalfkgBcMAMfAOFSvD4y0gIQxF/XzEyLkjXaRiUpibVj4+Cf3tUA==" } }, "dependencies": { @@ -66,6 +72,11 @@ "version": "1.8.0", "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.8.0.tgz", "integrity": "sha512-gwhMjFCQiYs3x/Sf+d49f10ERXaEFCPr+nVTryhAW8DWbMGqJqt9G4XuIaHmFW08zYvhgdzqXGr8AlW8v8dQkA==" + }, + "leaflet-contextmenu": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/leaflet-contextmenu/-/leaflet-contextmenu-1.4.0.tgz", + "integrity": "sha512-BXASCmJ5bLkuJGDCpWmvGqhZi5AzeOY0IbQalfkgBcMAMfAOFSvD4y0gIQxF/XzEyLkjXaRiUpibVj4+Cf3tUA==" } } } diff --git a/ts-client/package.json b/ts-client/package.json index 6e0f652..43ea662 100644 --- a/ts-client/package.json +++ b/ts-client/package.json @@ -4,6 +4,7 @@ }, "dependencies": { "geojson": "^0.5.0", - "leaflet": "^1.8.0" + "leaflet": "^1.8.0", + "leaflet-contextmenu": "^1.4.0" } }