vishnukvmd 1 year ago
parent
commit
94d6d34625

+ 7 - 0
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);

+ 1 - 0
apps/photos/src/components/Search/SearchBar/searchInput/valueContainerWithIcon.tsx

@@ -17,6 +17,7 @@ const getIconByType = (type: SuggestionType) => {
         case SuggestionType.DATE:
             return <CalendarIcon />;
         case SuggestionType.LOCATION:
+        case SuggestionType.CITY:
             return <LocationIcon />;
         case SuggestionType.COLLECTION:
             return <FolderIcon />;

+ 17 - 2
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

+ 50 - 5
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<City> = [];
+    private citiesPromise: Promise<void>;
 
     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;
+    }
+}

+ 34 - 5
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<Suggestion[]> {
     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 };
 

+ 4 - 0
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;

+ 0 - 17
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;
-    }
-}