diff --git a/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx b/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx
index 7db8d94ad..8b46f51ae 100644
--- a/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx
+++ b/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx
@@ -30,6 +30,7 @@ import { LocationTagData } from 'types/entity';
import { FILE_TYPE } from 'constants/file';
import { InputActionMeta } from 'react-select/src/types';
import { components } from 'react-select';
+import { City } from 'services/locationSearchService';
interface Iprops {
isOpen: boolean;
@@ -122,6 +123,12 @@ export default function SearchInput(props: Iprops) {
};
props.setIsOpen(true);
break;
+ case SuggestionType.CITY:
+ search = {
+ city: selectedOption.value as City,
+ };
+ props.setIsOpen(true);
+ break;
case SuggestionType.COLLECTION:
search = { collection: selectedOption.value as number };
setValue(null);
diff --git a/apps/photos/src/components/Search/SearchBar/searchInput/valueContainerWithIcon.tsx b/apps/photos/src/components/Search/SearchBar/searchInput/valueContainerWithIcon.tsx
index 8bf92a310..9ebe3cd58 100644
--- a/apps/photos/src/components/Search/SearchBar/searchInput/valueContainerWithIcon.tsx
+++ b/apps/photos/src/components/Search/SearchBar/searchInput/valueContainerWithIcon.tsx
@@ -17,6 +17,7 @@ const getIconByType = (type: SuggestionType) => {
case SuggestionType.DATE:
return ;
case SuggestionType.LOCATION:
+ case SuggestionType.CITY:
return ;
case SuggestionType.COLLECTION:
return ;
diff --git a/apps/photos/src/pages/gallery/index.tsx b/apps/photos/src/pages/gallery/index.tsx
index 82731dfdc..e38ddc657 100644
--- a/apps/photos/src/pages/gallery/index.tsx
+++ b/apps/photos/src/pages/gallery/index.tsx
@@ -120,7 +120,7 @@ import GalleryEmptyState from 'components/GalleryEmptyState';
import AuthenticateUserModal from 'components/AuthenticateUserModal';
import useMemoSingleThreaded from '@ente/shared/hooks/useMemoSingleThreaded';
import { isArchivedFile } from 'utils/magicMetadata';
-import { isSameDayAnyYear, isInsideLocationTag } from 'utils/search';
+import { isSameDayAnyYear } from 'utils/search';
import { getSessionExpiredMessage } from 'utils/ui';
import { syncEntities } from 'services/entityService';
import { constructUserIDToEmailMap } from 'services/collectionService';
@@ -131,7 +131,10 @@ import { ClipService } from 'services/clipService';
import isElectron from 'is-electron';
import downloadManager from 'services/download';
import { APPS } from '@ente/shared/apps/constants';
-import locationSearchService from 'services/locationSearchService';
+import locationSearchService, {
+ isInsideCity,
+ isInsideLocationTag,
+} from 'services/locationSearchService';
export const DeadCenter = styled('div')`
flex: 1;
@@ -518,6 +521,18 @@ export default function Gallery() {
) {
return false;
}
+ if (
+ search?.city &&
+ !isInsideCity(
+ {
+ latitude: item.metadata.latitude,
+ longitude: item.metadata.longitude,
+ },
+ search.city
+ )
+ ) {
+ return false;
+ }
if (
search?.person &&
search.person.files.indexOf(item.id) === -1
diff --git a/apps/photos/src/services/locationSearchService.ts b/apps/photos/src/services/locationSearchService.ts
index 377c4efed..454d07002 100644
--- a/apps/photos/src/services/locationSearchService.ts
+++ b/apps/photos/src/services/locationSearchService.ts
@@ -1,23 +1,68 @@
import { CITIES_URL } from '@ente/shared/constants/urls';
+import { LocationTagData } from 'types/entity';
+import { Location } from 'types/upload';
-interface City {
+export interface City {
city: string;
country: string;
lat: number;
lng: number;
}
+const DEFAULT_CITY_RADIUS = 10;
+
class LocationSearchService {
private cities: Array = [];
+ private citiesPromise: Promise;
loadCities() {
- fetch(CITIES_URL).then((response) => {
- response.json().then((data) => {
- this.cities = data;
- console.log(this.cities);
+ if (this.citiesPromise) {
+ return;
+ }
+ this.citiesPromise = fetch(CITIES_URL).then((response) => {
+ return response.json().then((data) => {
+ this.cities = data['data'];
});
});
}
+
+ async searchCities(searchTerm: string) {
+ if (!this.citiesPromise) {
+ this.loadCities();
+ }
+ await this.citiesPromise;
+ return this.cities.filter((city) => {
+ return city.city.toLowerCase().includes(searchTerm.toLowerCase());
+ });
+ }
}
export default new LocationSearchService();
+
+export function isInsideLocationTag(
+ location: Location,
+ locationTag: LocationTagData
+) {
+ const { centerPoint, aSquare, bSquare } = locationTag;
+ const { latitude, longitude } = location;
+ const x = Math.abs(centerPoint.latitude - latitude);
+ const y = Math.abs(centerPoint.longitude - longitude);
+ if ((x * x) / aSquare + (y * y) / bSquare <= 1) {
+ return true;
+ } else {
+ return false;
+ }
+}
+
+// TODO: Verify correctness
+export function isInsideCity(location: Location, city: City) {
+ const { lat, lng } = city;
+ const { latitude, longitude } = location;
+ const x = Math.abs(lat - latitude);
+ const y = Math.abs(lng - longitude);
+ if (x * x + y * y <= DEFAULT_CITY_RADIUS * DEFAULT_CITY_RADIUS) {
+ return true;
+ } else {
+ return false;
+ }
+}
diff --git a/apps/photos/src/services/searchService.ts b/apps/photos/src/services/searchService.ts
index 90665dd0b..42b91614d 100644
--- a/apps/photos/src/services/searchService.ts
+++ b/apps/photos/src/services/searchService.ts
@@ -16,11 +16,7 @@ import {
ClipSearchScores,
} from 'types/search';
import ObjectService from './machineLearning/objectService';
-import {
- getFormattedDate,
- isInsideLocationTag,
- isSameDayAnyYear,
-} from 'utils/search';
+import { getFormattedDate, isSameDayAnyYear } from 'utils/search';
import { Person, Thing } from 'types/machineLearning';
import { getUniqueFiles } from 'utils/file';
import { getLatestEntities } from './entityService';
@@ -31,6 +27,11 @@ import { ClipService, computeClipMatchScore } from './clipService';
import { CustomError } from '@ente/shared/error';
import { Model } from 'types/embedding';
import { getLocalEmbeddings } from './embeddingService';
+import locationSearchService, {
+ City,
+ isInsideCity,
+ isInsideLocationTag,
+} from './locationSearchService';
const DIGITS = new Set(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']);
@@ -61,6 +62,7 @@ export const getAutoCompleteSuggestions =
getFileNameSuggestion(searchPhrase, files),
getFileCaptionSuggestion(searchPhrase, files),
...(await getLocationTagSuggestions(searchPhrase)),
+ ...(await getCitySuggestions(searchPhrase)),
...(await getThingSuggestion(searchPhrase)),
].filter((suggestion) => !!suggestion);
@@ -279,6 +281,21 @@ async function getLocationTagSuggestions(searchPhrase: string) {
);
}
+async function getCitySuggestions(searchPhrase: string) {
+ const searchResults = await locationSearchService.searchCities(
+ searchPhrase
+ );
+
+ return searchResults.map(
+ (city) =>
+ ({
+ type: SuggestionType.CITY,
+ value: city,
+ label: city.city,
+ } as Suggestion)
+ );
+}
+
async function getThingSuggestion(searchPhrase: string): Promise {
const thingResults = await searchThing(searchPhrase);
@@ -425,6 +442,15 @@ function isSearchedFile(file: EnteFile, search: Search) {
search.location
);
}
+ if (search?.city) {
+ return isInsideCity(
+ {
+ latitude: file.metadata.latitude,
+ longitude: file.metadata.longitude,
+ },
+ search.city
+ );
+ }
if (search?.files) {
return search.files.indexOf(file.id) !== -1;
}
@@ -460,6 +486,9 @@ function convertSuggestionToSearchQuery(option: Suggestion): Search {
location: option.value as LocationTagData,
};
+ case SuggestionType.CITY:
+ return { city: option.value as City };
+
case SuggestionType.COLLECTION:
return { collection: option.value as number };
diff --git a/apps/photos/src/types/search/index.ts b/apps/photos/src/types/search/index.ts
index 1e41d53f1..2e6c94f48 100644
--- a/apps/photos/src/types/search/index.ts
+++ b/apps/photos/src/types/search/index.ts
@@ -3,6 +3,7 @@ import { IndexStatus } from 'types/machineLearning/ui';
import { EnteFile } from 'types/file';
import { LocationTagData } from 'types/entity';
import { FILE_TYPE } from 'constants/file';
+import { City } from 'services/locationSearchService';
export enum SuggestionType {
DATE = 'DATE',
@@ -16,6 +17,7 @@ export enum SuggestionType {
FILE_CAPTION = 'FILE_CAPTION',
FILE_TYPE = 'FILE_TYPE',
CLIP = 'CLIP',
+ CITY = 'CITY',
}
export interface DateValue {
@@ -35,6 +37,7 @@ export interface Suggestion {
| Thing
| WordGroup
| LocationTagData
+ | City
| FILE_TYPE
| ClipSearchScores;
hide?: boolean;
@@ -43,6 +46,7 @@ export interface Suggestion {
export type Search = {
date?: DateValue;
location?: LocationTagData;
+ city?: City;
collection?: number;
files?: number[];
person?: Person;
diff --git a/apps/photos/src/utils/search/index.ts b/apps/photos/src/utils/search/index.ts
index 891c99254..6392e4840 100644
--- a/apps/photos/src/utils/search/index.ts
+++ b/apps/photos/src/utils/search/index.ts
@@ -1,6 +1,4 @@
-import { LocationTagData } from 'types/entity';
import { DateValue } from 'types/search';
-import { Location } from 'types/upload';
export const isSameDayAnyYear =
(baseDate: DateValue) => (compareDate: Date) => {
@@ -28,18 +26,3 @@ export function getFormattedDate(date: DateValue) {
new Date(date.year ?? 1, date.month ?? 1, date.date ?? 1)
);
}
-
-export function isInsideLocationTag(
- location: Location,
- locationTag: LocationTagData
-) {
- const { centerPoint, aSquare, bSquare } = locationTag;
- const { latitude, longitude } = location;
- const x = Math.abs(centerPoint.latitude - latitude);
- const y = Math.abs(centerPoint.longitude - longitude);
- if ((x * x) / aSquare + (y * y) / bSquare <= 1) {
- return true;
- } else {
- return false;
- }
-}