City parse 1

This commit is contained in:
Manav Rathi
2024-09-06 16:52:28 +05:30
parent 77ac215b76
commit fc95069421
4 changed files with 76 additions and 17 deletions

View File

@@ -31,7 +31,7 @@ class LocationSearchService {
}
await this.citiesPromise;
return this.cities.filter((city) => {
return city.city
return city.name
.toLowerCase()
.startsWith(searchTerm.toLowerCase());
});

View File

@@ -220,7 +220,7 @@ async function getLocationSuggestions(searchPhrase: string) {
await locationSearchService.searchCities(searchPhrase);
const nonConflictingCityResult = citySearchResults.filter(
(city) => !locationTagNames.has(city.city),
(city) => !locationTagNames.has(city.name),
);
const citySearchSuggestions = nonConflictingCityResult.map(
@@ -228,7 +228,7 @@ async function getLocationSuggestions(searchPhrase: string) {
({
type: SuggestionType.CITY,
value: city,
label: city.city,
label: city.name,
}) as Suggestion,
);

View File

@@ -3,6 +3,7 @@
* and the search worker that does the actual searching (`worker.ts`).
*/
import type { Location } from "@/base/types";
import { FileType } from "@/media/file-type";
import type { MLStatus } from "@/new/photos/services/ml";
import type { EnteFile } from "@/new/photos/types/file";
@@ -71,12 +72,24 @@ export interface LocationTagData {
centerPoint: LocationOld;
}
export interface City {
city: string;
country: string;
lat: number;
lng: number;
}
/**
* A city as identified by a static dataset.
*
* Each city is represented by its latitude and longitude. The dataset does not
* have information about the city's estimated radius.
*/
export type City = Location & {
/**
* Name of the city.
*/
name: string;
/**
* Name of the city, lowercased.
*
* Precomputing this save an lowercasing during the search itself.
*/
lowercasedName: string;
};
export enum SuggestionType {
DATE = "DATE",

View File

@@ -1,6 +1,7 @@
// TODO-cgroups
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
import { HTTPError } from "@/base/http";
import { fileCreationPhotoDate, fileLocation } from "@/media/file-metadata";
import type { EnteFile } from "@/new/photos/types/file";
import { wait } from "@/utils/promise";
@@ -9,6 +10,7 @@ import { getPublicMagicMetadataSync } from "@ente/shared/file-metadata";
import type { Component } from "chrono-node";
import * as chrono from "chrono-node";
import { expose } from "comlink";
import { z } from "zod";
import type {
City,
DateSearchResult,
@@ -26,6 +28,8 @@ import { SuggestionType } from "./types";
*/
export class SearchWorker {
private enteFiles: EnteFile[] = [];
private cities: City[] = [];
private citiesPromise: Promise<void> | undefined;
/**
* Set the files that we should search across.
@@ -42,7 +46,18 @@ export class SearchWorker {
locale: string,
holidays: DateSearchResult[],
) {
return createSearchQuery(searchString, locale, holidays);
this.triggerCityFetchIfNeeded();
return createSearchQuery(searchString, locale, holidays, this.cities);
}
/**
* Lazily trigger a fetch of city data, but don't wait for it to complete.
*/
triggerCityFetchIfNeeded() {
if (this.citiesPromise) return;
this.citiesPromise = fetchCities().then((cs) => {
this.cities = cs;
});
}
/**
@@ -59,6 +74,7 @@ const createSearchQuery = async (
searchString: string,
locale: string,
holidays: DateSearchResult[],
cities: City[],
): Promise<Suggestion[]> => {
// Normalize it by trimming whitespace and converting to lowercase.
const s = searchString.trim().toLowerCase();
@@ -66,10 +82,10 @@ const createSearchQuery = async (
// TODO Temp
await wait(0);
return [dateSuggestion(s, locale, holidays)].flat();
return [dateSuggestions(s, locale, holidays)].flat();
};
const dateSuggestion = (
const dateSuggestions = (
s: string,
locale: string,
holidays: DateSearchResult[],
@@ -154,6 +170,40 @@ const parseYearComponents = (s: string): DateSearchResult[] => {
const parseHolidayComponents = (s: string, holidays: DateSearchResult[]) =>
holidays.filter(({ label }) => label.toLowerCase().includes(s));
/**
* Zod schema describing world_cities.json.
*
* The entries also have a country field which we don't currently use.
*/
const RemoteWorldCities = z.object({
data: z.array(
z.object({
city: z.string(),
lat: z.number(),
lng: z.number(),
}),
),
});
const fetchCities = async () => {
const res = await fetch("https://static.ente.io/world_cities.json");
if (!res.ok) throw new HTTPError(res);
return RemoteWorldCities.parse(await res.json()).data.map(
({ city, lat, lng }) => ({
name: city,
lowercasedName: city.toLowerCase(),
latitude: lat,
longitude: lng,
}),
);
};
/**
* Return all cities whose name begins with the given search string.
*/
const matchingCities = (s: string, cities: City[]) =>
cities.filter(({ lowercasedName }) => lowercasedName.startsWith(s));
const isMatch = (file: EnteFile, query: SearchQuery) => {
if (query?.collection) {
return query.collection === file.collectionID;
@@ -226,11 +276,7 @@ const isInsideLocationTag = (
) => isWithinRadius(location, locationTag.centerPoint, locationTag.radius);
const isInsideCity = (location: LocationOld, city: City) =>
isWithinRadius(
{ latitude: city.lat, longitude: city.lng },
location,
defaultCityRadius,
);
isWithinRadius(city, location, defaultCityRadius);
const isWithinRadius = (
centerPoint: LocationOld,