Merge pull request #92 from knom/feature-OSM-search

Feature: Added OpenStreetMap Search (Missing-locations)
This commit is contained in:
Varun Raj Manoharan
2025-01-26 16:41:14 +05:30
committed by GitHub
4 changed files with 206 additions and 16 deletions
@@ -12,6 +12,7 @@ import { IPlace } from "@/types/common";
import React, { useState } from "react";
import TagMissingLocationSearchAndAdd from "./TagMissingLocationSearchAndAdd";
import TagMissingLocationSearchLatLong from "./TagMissingLocationSearchLatLong";
import TagMissingLocationOSMSearchAndAdd from "./TagMissingLocationOSMSearchAndAdd";
import dynamic from "next/dynamic";
import { useMissingLocationContext } from "@/contexts/MissingLocationContext";
@@ -25,15 +26,14 @@ interface ITagMissingLocationDialogProps {
export default function TagMissingLocationDialog({
onSubmit,
}: ITagMissingLocationDialogProps) {
const {selectedIds} = useMissingLocationContext();
const { selectedIds } = useMissingLocationContext();
const [open, setOpen] = useState(false);
const [mapPosition,setMapPosition] = useState<IPlace>({
latitude: 48.0,
longitude: 16.0,
name: "home"
const [mapPosition, setMapPosition] = useState<IPlace>({
latitude: 48.210033,
longitude: 16.363449,
name: "Vienna, Austria"
});
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild >
@@ -46,20 +46,21 @@ export default function TagMissingLocationDialog({
Tagging a location will add the location to the selected assets.
</DialogDescription>
</DialogHeader>
<Tabs defaultValue="search">
<TabsList className="flex justify-center">
<TabsTrigger value="search">Search and Pick</TabsTrigger>
<TabsTrigger value="latlong">
Lat & Long
</TabsTrigger>
<Tabs defaultValue="search" className="border rounded-lg">
<TabsList className="flex justify-between">
<TabsTrigger value="search">Immich Search</TabsTrigger>
<TabsTrigger value="searchOsm">OSM Search</TabsTrigger>
<TabsTrigger value="latlong">Lat &amp; Long</TabsTrigger>
<TabsTrigger value="maps">Map</TabsTrigger>
</TabsList>
<TabsContent value="search">
<TagMissingLocationSearchAndAdd onSubmit={onSubmit} onOpenChange={setOpen} />
</TabsContent>
<TabsContent value="searchOsm">
<TagMissingLocationOSMSearchAndAdd onSubmit={onSubmit} onOpenChange={setOpen} location={mapPosition} onLocationChange={setMapPosition} />
</TabsContent>
<TabsContent value="latlong">
<TagMissingLocationSearchLatLong onSubmit={onSubmit}
onOpenChange={setOpen} location={mapPosition} onLocationChange={setMapPosition} />
<TagMissingLocationSearchLatLong onSubmit={onSubmit} onOpenChange={setOpen} location={mapPosition} onLocationChange={setMapPosition} />
</TabsContent>
<TabsContent value="maps">
<div className="flex flex-col gap-6 items-center ">
@@ -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<any>;
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<IPlace[] | null>(null);
const searchTimer = useRef<NodeJS.Timeout>();
const [loading, setLoading] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [selectedPlace, setSelectedPlace] = useState<IPlace | null>(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: 5, // Limit the results to 1
},
});
if (response.data.length > 0) {
const data = (response.data) as Array<any>;
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 (
<div className="flex flex-col gap-4 py-4 px-2">
<div className="flex flex-col gap-2">
<Label>Search Location</Label>
<Input
placeholder="Search location"
onChange={(e) => {
handleSearch(e.target.value);
}}
/>
</div>
<div>
{loading && <Loader />}
{!loading && searchedPlaces && searchedPlaces.length === 0 && (
<ErrorBlock description="No results found" />
)}
{!loading && searchedPlaces && searchedPlaces.length > 0 && (
<div className="flex flex-col gap-2 max-h-[400px] overflow-y-auto">
{searchedPlaces.map((place) => (
<div
key={place.name}
onClick={() => handleSelect(place)}
className={cn(
"hover:bg-gray-300 flex justify-between items-center px-2 py-1 rounded-lg cursor-pointer",
{
"bg-gray-300":
selectedPlace && selectedPlace.name === place.name,
}
)}
>
<div>
<p>{place.name}</p>
<span className="text-xs text-gray-600">
{place.latitude}, {place.longitude}
</span>
</div>
{selectedPlace && selectedPlace.name === place.name && (
<Check className="text-green-500" />
)}
</div>
))}
</div>
)}
</div>
<div className="self-end">
<Button
variant="outline"
onClick={handleSubmit}
disabled={!selectedPlace || submitting}
>
Add New Location
</Button>
</div>
</div>
)
}
@@ -1,3 +1,4 @@
import ErrorBlock from "@/components/shared/ErrorBlock";
import { Button } from "@/components/ui/button";
+1 -1
View File
@@ -41,7 +41,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-6xl translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}