From 5ea7e1f34458431cc9e99aaa163df832a36c0840 Mon Sep 17 00:00:00 2001 From: knom Date: Wed, 18 Dec 2024 09:46:34 +0000 Subject: [PATCH 1/3] Added OpenStreetMap Search --- .../TagMissingLocationDialog.tsx | 17 +- .../TagMissingLocationOSMSearchAndAdd.tsx | 188 ++++++++++++++++++ .../TagMissingLocationSearchLatLong.tsx | 34 +++- src/components/ui/dialog.tsx | 2 +- 4 files changed, 228 insertions(+), 13 deletions(-) create mode 100644 src/components/assets/missing-location/TagMissingLocationDialog/TagMissingLocationOSMSearchAndAdd.tsx diff --git a/src/components/assets/missing-location/TagMissingLocationDialog/TagMissingLocationDialog.tsx b/src/components/assets/missing-location/TagMissingLocationDialog/TagMissingLocationDialog.tsx index 3590b01..f5f2fd7 100644 --- a/src/components/assets/missing-location/TagMissingLocationDialog/TagMissingLocationDialog.tsx +++ b/src/components/assets/missing-location/TagMissingLocationDialog/TagMissingLocationDialog.tsx @@ -13,6 +13,7 @@ import React, { useState } from "react"; import TagMissingLocationSearchAndAdd from "./TagMissingLocationSearchAndAdd"; import TagMissingLocationSearchLatLong from "./TagMissingLocationSearchLatLong"; import { MapPinCheck } from "lucide-react"; +import TagMissingLocationOSMSearchAndAdd from "./TagMissingLocationOSMSearchAndAdd"; interface ITagMissingLocationDialogProps { onSubmit: (place: IPlace) => Promise; @@ -23,6 +24,11 @@ export default function TagMissingLocationDialog({ const [open, setOpen] = useState(false); + const [mapPosition,setMapPosition] = useState({ + latitude: 48.0, + longitude: 16.0, + name: "home1" + }); return ( @@ -38,18 +44,21 @@ export default function TagMissingLocationDialog({ - Search and Pick - + Immich Search + OSM Search + Latitude and Longitude - Google Maps + + + - +
diff --git a/src/components/assets/missing-location/TagMissingLocationDialog/TagMissingLocationOSMSearchAndAdd.tsx b/src/components/assets/missing-location/TagMissingLocationDialog/TagMissingLocationOSMSearchAndAdd.tsx new file mode 100644 index 0000000..9657033 --- /dev/null +++ b/src/components/assets/missing-location/TagMissingLocationDialog/TagMissingLocationOSMSearchAndAdd.tsx @@ -0,0 +1,188 @@ +import ErrorBlock from "@/components/shared/ErrorBlock"; +import { Button } from "@/components/ui/button"; + +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import Loader from "@/components/ui/loader"; +import { useToast } from "@/components/ui/use-toast"; +import { cn } from "@/lib/utils"; +import { IPlace } from "@/types/common"; +import axios from "axios"; +import { Check } from "lucide-react"; +import React, { useRef, useState } from "react"; + +interface ITagMissingLocationSearchAndAddProps { + onSubmit: (place: IPlace) => Promise; + onOpenChange: (open: boolean) => void; + location: IPlace; + onLocationChange: (place: IPlace) => void; +} + +export default function TagMissingLocationOSMSearchAndAdd( + { + onSubmit, + onOpenChange, + location, + onLocationChange + }: ITagMissingLocationSearchAndAddProps +) { + const { toast } = useToast(); + const [searchedPlaces, setSearchedPlaces] = useState(null); + const searchTimer = useRef(); + const [loading, setLoading] = useState(false); + const [submitting, setSubmitting] = useState(false); + const [selectedPlace, setSelectedPlace] = useState(null); + + const searchPlaces = async (value: string) => { + try { + const response = await axios.get("https://nominatim.openstreetmap.org/search", { + params: { + q: value, + format: "json", // Response in JSON format + addressdetails: 1, // Include address details + limit: 1, // Limit the results to 1 + }, + }); + + if (response.data.length > 0) { + const data = (response.data) as Array; + + const places = data.map((result): IPlace => ({ + name: result.display_name as string, // Full address or place name + latitude: parseFloat(result.lat), + longitude: parseFloat(result.lon), + })); + + return places; + } else { + console.log("No results found for the location."); + return null; + } + } catch (error) { + console.error("Error fetching coordinates:", error); + return null; + } + } + + const handleSearch = (value: string) => { + if (searchTimer.current) { + clearTimeout(searchTimer.current); + } + + searchTimer.current = setTimeout(() => { + if (!value || !value.trim().length) { + setSearchedPlaces(null); + return; + } + setSelectedPlace(null); + setSearchedPlaces([]); + setLoading(true); + if (!value) { + setSearchedPlaces([]); + setLoading(false); + return; + } + + return searchPlaces(value) + .then(setSearchedPlaces) + .finally(() => { + setLoading(false); + }); + }, 500); + }; + + const handleSelect = (place: IPlace) => { + if (selectedPlace && selectedPlace.name === place.name) { + setSelectedPlace(null); + } else { + setSelectedPlace(place); + debugger; + onLocationChange(place); + } + }; + + const handleSubmit = () => { + if (!selectedPlace) { + return; + } + + setSubmitting(true); + onSubmit(selectedPlace) + .then(() => { + toast({ + title: "Location updated", + description: "Location updated successfully", + }); + }) + .then(() => { + onOpenChange(false); + setSelectedPlace(null); + }) + .catch(() => { + toast({ + title: "Error", + description: "Error updating location", + }); + }) + .finally(() => { + setSubmitting(false); + }); + }; + + + return ( +
+
+ + { + handleSearch(e.target.value); + }} + /> +
+
+ {loading && } + {!loading && searchedPlaces && searchedPlaces.length === 0 && ( + + )} + {!loading && searchedPlaces && searchedPlaces.length > 0 && ( +
+ {searchedPlaces.map((place) => ( +
handleSelect(place)} + className={cn( + "hover:bg-zinc-900 flex justify-between items-center px-2 py-1 rounded-lg cursor-pointer", + { + "bg-zinc-900": + selectedPlace && selectedPlace.name === place.name, + } + )} + > +
+

{place.name}

+ + {place.latitude}, {place.longitude} + +
+ {selectedPlace && selectedPlace.name === place.name && ( + + )} +
+ ))} +
+ )} +
+
+ +
+
+ ) +} diff --git a/src/components/assets/missing-location/TagMissingLocationDialog/TagMissingLocationSearchLatLong.tsx b/src/components/assets/missing-location/TagMissingLocationDialog/TagMissingLocationSearchLatLong.tsx index d0098dc..d31bd90 100644 --- a/src/components/assets/missing-location/TagMissingLocationDialog/TagMissingLocationSearchLatLong.tsx +++ b/src/components/assets/missing-location/TagMissingLocationDialog/TagMissingLocationSearchLatLong.tsx @@ -1,3 +1,4 @@ + import ErrorBlock from "@/components/shared/ErrorBlock"; import { Button } from "@/components/ui/button"; @@ -8,25 +9,30 @@ import { useToast } from "@/components/ui/use-toast"; import { searchPlaces } from "@/handlers/api/common.handler"; import { cn } from "@/lib/utils"; import { IPlace } from "@/types/common"; -import clsx from "clsx"; import { Check } from "lucide-react"; import React, { useMemo, useRef, useState } from "react"; interface TagMissingLocationSearchLatLongProps { onSubmit: (place: IPlace) => Promise; onOpenChange: (open: boolean) => void; + location: IPlace; + onLocationChange: (place: IPlace) => void; } + export default function TagMissingLocationSearchLatLong( { onSubmit, - onOpenChange + onOpenChange, + location, + onLocationChange }: TagMissingLocationSearchLatLongProps ) { const { toast } = useToast(); + const [formData, setFormData] = useState({ - latitude: 0, - longitude: 0, - name: "", + latitude: location.latitude, + longitude: location.longitude, + name: location.name, }); const [submitting, setSubmitting] = useState(false); @@ -41,7 +47,8 @@ export default function TagMissingLocationSearchLatLong( }, [formData]); const handleChange = (key: string, value: string) => { - if (value.includes(",")) { + // if somebody pastes a combination of "lat, long" + if (key === "latitude" && value.includes(",")) { const latLong = value.split(","); if (latLong.length === 2) { setFormData({ @@ -51,7 +58,6 @@ export default function TagMissingLocationSearchLatLong( }); } } else { - setFormData({ ...formData, [key]: value, @@ -92,6 +98,16 @@ export default function TagMissingLocationSearchLatLong( }; + const handleBlur = () => { + const place :IPlace = { + name: formData.name, + latitude: parseFloat(formData.latitude as unknown as string), + longitude: parseFloat(formData.longitude as unknown as string) + }; + + onLocationChange(place); + } + return (
@@ -103,6 +119,7 @@ export default function TagMissingLocationSearchLatLong( onChange={(e) => { handleChange("latitude", e.target.value); }} + onBlur={(e) => handleBlur()} />
@@ -115,10 +132,11 @@ export default function TagMissingLocationSearchLatLong( onChange={(e) => { handleChange("longitude", e.target.value); }} + onBlur={(e) => handleBlur()} />
- +