src/lib/locations.ts
Rama main · réplica de lectura del repositorio original.
// A place, between the geocoder and the two forms that record one. Both the
// dive site form and a trip part's row fill the same object from the same
// response, so the mapping is written once here rather than slightly
// differently in each of them.
import type { GeocodeResult } from "@/lib/api/geocoding";
import type { LocationFormValue } from "@/lib/validations/location";
/**
* A geocoder result as a place.
*
* `name` is the API's `location` as it arrives - the place, its region and its
* country, "Ko Tao, Surat Thani Province, Thailand" - for the site dialog and
* the trip form alike. The API composes it once, for a typed search and a
* dropped pin both, so nothing here rebuilds it from parts.
*
* The centre and the box are the *place's*, which is what a forward search
* answers with. A reverse geocode must not go through here: the coordinates it
* returns are the host's own position, so adopting them would file the pin a
* diver dropped as the centre of the town around it.
*/
export function geocodeResultToLocation(
result: GeocodeResult,
): LocationFormValue {
return {
name: result.location,
latitude: result.latitude,
longitude: result.longitude,
bbox_south: result.bbox_south,
bbox_north: result.bbox_north,
bbox_west: result.bbox_west,
bbox_east: result.bbox_east,
};
}
/**
* Where a place sits, as one string: `region, country` where both are known,
* whichever one is where only one is, and `null` where neither is.
*
* What the dive site search shows for a geocoder row.
*/
export function formatPlaceContext(
region?: string | null,
country?: string | null,
): string | null {
const parts = [region, country].filter(
(part): part is string => !!part?.trim(),
);
return parts.length > 0 ? parts.join(", ") : null;
}
/**
* A geocoder result as the dive site search shows it: the place's own name, and
* where it is as `region, country` - "Moalboal" and "Cebu, Philippines", read
* together as "Moalboal, Cebu, Philippines".
*
* Read as one line, the row says what picking it saves: the API's `location` is
* the same three parts. A result that matched an address has no name of its
* own, so it reads as its `location` alone, which already holds every part a
* hint would add. A context part the name already says is dropped - the country
* row "Philippines" gets no context at all.
*/
export function geocodeResultLabel(result: GeocodeResult): {
name: string;
context: string | null;
} {
const name = result.name;
if (!name) return { name: result.location, context: null };
const region = unrepeated(result.region, name);
const country = unrepeated(result.country, name, region);
return { name, context: formatPlaceContext(region, country) };
}
/**
* `part`, unless it only repeats a comma-separated part of one of `labels` -
* compared whole and case-insensitively, so "Cebu" repeats "Moalboal, Cebu" but
* not "Cebu City". `null` for a repeat and for a blank.
*
* A menu row joins its name and hint with ", ", so a hint part the name already
* holds reads twice: the country row "Philippines" would otherwise say
* "Philippines, Philippines".
*/
export function unrepeated(
part: string | null | undefined,
...labels: (string | null | undefined)[]
): string | null {
const trimmed = part?.trim();
if (!trimmed) return null;
const said = labels.flatMap((label) =>
(label ?? "").split(",").map((each) => each.trim().toLowerCase()),
);
return said.includes(trimmed.toLowerCase()) ? null : trimmed;
}