Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion components/CatalogFilterSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,13 @@ export default function CatalogFilterSidebar({ variant, collapsed = false, onTog
async (loc: FetchLocation.Location) => {
setShowLocationDropdown(false);
setLocationInput("");
const detail = await lookupLocation(loc.id);
const detail =
loc.position && Number.isFinite(loc.position.lat) && Number.isFinite(loc.position.lng)
? {
title: loc.title,
position: loc.position,
}
: await lookupLocation(loc.id);
if (!detail) return;
setLocationLabel(detail.title);
const radius = router.query.nearDistanceKm ? String(router.query.nearDistanceKm) : "50";
Expand Down
64 changes: 52 additions & 12 deletions components/SearchLocation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,30 +34,70 @@ export default function SearchLocation(props: Props) {
}, []);

const [options, setOptions] = useState<Array<SelectOption>>([]);
const [searchResults, setSearchResults] = useState<Array<FetchLocation.Location>>([]);
const [loading, setLoading] = useState<boolean>(false);

const toCoordValue = useCallback((location: FetchLocation.Location): string => {
return location.position
? `COORD:${location.position.lat},${location.position.lng}|${encodeURIComponent(location.title)}`
: location.id;
}, []);

useEffect(() => {
const searchLocation = async () => {
setLoading(true);
setOptions(createOptionsFromResult(await fetchLocation(inputValue)));
const results = await fetchLocation(inputValue);
setSearchResults(results);
setOptions(
results.map(location => ({
value: toCoordValue(location),
label: location.title,
}))
);
setLoading(false);
};
searchLocation();
}, [inputValue]);

function createOptionsFromResult(result: Array<FetchLocation.Location>): Array<SelectOption> {
return result.map(location => {
return {
value: location.id,
label: location.title,
};
});
}
}, [inputValue, toCoordValue]);

/* Handling selection */

async function handleSelect(selected: string[]) {
const location = await lookupLocation(selected[0]);
const selectedValue = selected[0]?.trim();
if (!selectedValue) {
onSelect(null);
return;
}

const matchedResult = searchResults.find(location => toCoordValue(location) === selectedValue);
if (matchedResult?.position) {
onSelect({
title: matchedResult.title,
id: matchedResult.id,
language: matchedResult.language,
resultType: matchedResult.resultType,
administrativeAreaType: matchedResult.administrativeAreaType,
address: {
label: matchedResult.address.label,
countryCode: matchedResult.address.countryCode,
countryName: matchedResult.address.countryName,
state: "",
},
position: {
lat: matchedResult.position.lat,
lng: matchedResult.position.lng,
},
mapView: {
west: matchedResult.position.lng,
south: matchedResult.position.lat,
east: matchedResult.position.lng,
north: matchedResult.position.lat,
},
});
setInputValue("");
return;
}

const location = await lookupLocation(selectedValue);
if (!location) onSelect(null);
else onSelect(location);
setInputValue("");
Expand Down
176 changes: 170 additions & 6 deletions lib/fetchLocation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,134 @@ import { formatSelectOption, SelectOption } from "components/brickroom/utils/BrS

//

interface NominatimAddress {
country?: string;
country_code?: string;
state?: string;
}

interface NominatimItem {
place_id?: number;
osm_type?: "node" | "way" | "relation";
osm_id?: number;
display_name?: string;
lat?: string;
lon?: string;
boundingbox?: [string, string, string, string];
address?: NominatimAddress;
}

function osmTypePrefix(osmType?: string): "N" | "W" | "R" | "" {
if (osmType === "node") return "N";
if (osmType === "way") return "W";
if (osmType === "relation") return "R";
return "";
}

function toLocationId(item: NominatimItem): string {
const prefix = osmTypePrefix(item.osm_type);
if (prefix && item.osm_id) return `${prefix}${item.osm_id}`;
if (item.lat && item.lon) {
const title = encodeURIComponent(item.display_name || "");
return `COORD:${item.lat},${item.lon}|${title}`;
}
return "";
}

function isValidOsmId(id: string): boolean {
return /^[NWR]\d+$/.test(id);
}

function parseCoordId(id: string): { lat: number; lng: number; title: string } | null {
if (!id.startsWith("COORD:")) return null;

const payload = id.slice(6);
const [coords, encodedTitle = ""] = payload.split("|");
const [latStr, lngStr] = coords.split(",");
const lat = Number(latStr);
const lng = Number(lngStr);
if (!Number.isFinite(lat) || !Number.isFinite(lng)) return null;

return {
lat,
lng,
title: decodeURIComponent(encodedTitle || ""),
};
}

function mapSearchItem(item: NominatimItem): FetchLocation.Location {
const lat = Number(item.lat || 0);
const lng = Number(item.lon || 0);

return {
title: item.display_name || "",
id: toLocationId(item),
language: "",
resultType: item.osm_type || "",
administrativeAreaType: "",
address: {
label: item.display_name || "",
countryCode: (item.address?.country_code || "").toUpperCase(),
countryName: item.address?.country || "",
},
highlights: {
title: [],
address: {
label: [],
countryCode: [],
},
},
position: {
lat,
lng,
},
};
}

function mapLookupItem(item: NominatimItem): LocationLookup.Location {
const lat = Number(item.lat || 0);
const lng = Number(item.lon || 0);
const bbox = item.boundingbox;

return {
title: item.display_name || "",
id: toLocationId(item),
language: "",
resultType: item.osm_type || "",
administrativeAreaType: "",
address: {
label: item.display_name || "",
countryCode: (item.address?.country_code || "").toUpperCase(),
countryName: item.address?.country || "",
state: item.address?.state || "",
},
position: {
lat,
lng,
},
mapView: {
west: bbox ? Number(bbox[2]) : lng,
south: bbox ? Number(bbox[0]) : lat,
east: bbox ? Number(bbox[3]) : lng,
north: bbox ? Number(bbox[1]) : lat,
},
};
}

// Fetches the location from the API
export async function fetchLocation(text: string): Promise<Array<FetchLocation.Location>> {
if (!text) return [];

try {
const result = await fetch(`${process.env.NEXT_PUBLIC_LOCATION_AUTOCOMPLETE}?q=${encodeURI(text)}`);
const data = (await result.json()) as FetchLocation.Response;
return data?.items ? [...data.items] : [];
const params = new URLSearchParams({
q: text,
format: "jsonv2",
addressdetails: "1",
limit: "10",
});
const result = await fetch(`${process.env.NEXT_PUBLIC_LOCATION_AUTOCOMPLETE}?${params.toString()}`);
const data = (await result.json()) as Array<NominatimItem>;
return Array.isArray(data) ? data.map(mapSearchItem).filter(item => !!item.id) : [];
} catch {
return [];
}
Expand All @@ -37,11 +157,51 @@ export async function getLocationOptions(text: string): Promise<Array<SelectOpti
}

// Location lookup
export async function lookupLocation(id: string): Promise<LocationLookup.Location> {
export async function lookupLocation(id: string): Promise<LocationLookup.Location | null> {
if (!id) throw new Error("NoLocationId");

const response = await fetch(`${process.env.NEXT_PUBLIC_LOCATION_LOOKUP}?id=${encodeURI(id)}`);
return await response.json();
if (id.startsWith("here:")) return null;
const coord = parseCoordId(id);
if (coord) {
return {
title: coord.title,
id,
language: "",
resultType: "",
administrativeAreaType: "",
address: {
label: coord.title,
countryCode: "",
countryName: "",
state: "",
},
position: {
lat: coord.lat,
lng: coord.lng,
},
mapView: {
west: coord.lng,
south: coord.lat,
east: coord.lng,
north: coord.lat,
},
};
}
if (!isValidOsmId(id)) return null;

try {
const params = new URLSearchParams({
osm_ids: id,
format: "jsonv2",
addressdetails: "1",
});
const response = await fetch(`${process.env.NEXT_PUBLIC_LOCATION_LOOKUP}?${params.toString()}`);
const data = (await response.json()) as Array<NominatimItem>;
if (!Array.isArray(data) || data.length === 0) return null;
return mapLookupItem(data[0]);
} catch {
return null;
}
}

//
Expand Down Expand Up @@ -81,6 +241,10 @@ export namespace FetchLocation {
administrativeAreaType: string;
address: Address;
highlights: Highlights;
position?: {
lat: number;
lng: number;
};
}

export interface Response {
Expand Down
Loading