Refactor search code

In prep for moving location tags handling to @/new
This commit is contained in:
Manav Rathi
2024-09-05 19:19:24 +05:30
parent 9476d26972
commit 18e3adde11
20 changed files with 317 additions and 337 deletions

View File

@@ -1,8 +1,8 @@
import { Collection } from "types/collection";
import { SearchBarMobile } from "./searchBarMobile";
import { UpdateSearch } from "@/new/photos/services/search/types";
import { EnteFile } from "@/new/photos/types/file";
import { UpdateSearch } from "types/search";
import SearchInput from "./searchInput";
import { SearchBarWrapper } from "./styledComponents";

View File

@@ -1,10 +1,10 @@
import { PeopleList } from "@/new/photos/components/PeopleList";
import { isMLEnabled } from "@/new/photos/services/ml";
import { Suggestion, SuggestionType } from "@/new/photos/services/search/types";
import { Row } from "@ente/shared/components/Container";
import { Box, styled } from "@mui/material";
import { t } from "i18next";
import { components } from "react-select";
import { Suggestion, SuggestionType } from "types/search";
const { Menu } = components;

View File

@@ -1,9 +1,18 @@
import { FileType } from "@/media/file-type";
import { isMLEnabled } from "@/new/photos/services/ml";
import type {
City,
LocationTagData,
SearchDateComponents,
SearchPerson,
} from "@/new/photos/services/search/types";
import {
ClipSearchScores,
SearchOption,
SearchQuery,
SuggestionType,
UpdateSearch,
} from "@/new/photos/services/search/types";
import { EnteFile } from "@/new/photos/types/file";
import CloseIcon from "@mui/icons-material/Close";
import { IconButton } from "@mui/material";
@@ -15,20 +24,11 @@ import { useCallback, useContext, useEffect, useRef, useState } from "react";
import { components } from "react-select";
import AsyncSelect from "react-select/async";
import { InputActionMeta } from "react-select/src/types";
import { City } from "services/locationSearchService";
import {
getAutoCompleteSuggestions,
getDefaultOptions,
} from "services/searchService";
import { Collection } from "types/collection";
import { LocationTagData } from "types/entity";
import {
ClipSearchScores,
Search,
SearchOption,
SuggestionType,
UpdateSearch,
} from "types/search";
import { SelectStyles } from "../../../../styles/search";
import { SearchInputWrapper } from "../styledComponents";
import MenuWithPeople from "./MenuWithPeople";
@@ -116,7 +116,7 @@ export default function SearchInput(props: Iprops) {
if (!selectedOption) {
return;
}
let search: Search;
let search: SearchQuery;
switch (selectedOption.type) {
case SuggestionType.DATE:
search = {

View File

@@ -1,3 +1,4 @@
import { SearchOption } from "@/new/photos/services/search/types";
import {
FreeFlowText,
SpaceBetweenFlex,
@@ -6,7 +7,6 @@ import { Box, Divider, Stack, Typography } from "@mui/material";
import CollectionCard from "components/Collections/CollectionCard";
import { ResultPreviewTile } from "components/Collections/styledComponents";
import { t } from "i18next";
import { SearchOption } from "types/search";
import { components } from "react-select";

View File

@@ -1,3 +1,7 @@
import {
SearchOption,
SuggestionType,
} from "@/new/photos/services/search/types";
import { FlexWrapper } from "@ente/shared/components/Container";
import CalendarIcon from "@mui/icons-material/CalendarMonth";
import FolderIcon from "@mui/icons-material/Folder";
@@ -7,7 +11,6 @@ import SearchIcon from "@mui/icons-material/SearchOutlined";
import { Box } from "@mui/material";
import { components } from "react-select";
import { SelectComponents } from "react-select/src/components";
import { SearchOption, SuggestionType } from "types/search";
const { ValueContainer } = components;

View File

@@ -1,8 +1,8 @@
import { SearchResultSummary } from "@/new/photos/services/search/types";
import { Typography } from "@mui/material";
import { CollectionInfo } from "components/Collections/CollectionInfo";
import { CollectionInfoBarWrapper } from "components/Collections/styledComponents";
import { t } from "i18next";
import { SearchResultSummary } from "types/search";
interface Iprops {
searchResultSummary: SearchResultSummary;

View File

@@ -1,4 +1,5 @@
import { NavbarBase } from "@/base/components/Navbar";
import { UpdateSearch } from "@/new/photos/services/search/types";
import { EnteFile } from "@/new/photos/types/file";
import { FlexWrapper, HorizontalFlex } from "@ente/shared/components/Container";
import ArrowBack from "@mui/icons-material/ArrowBack";
@@ -8,7 +9,6 @@ import SearchBar from "components/Search/SearchBar";
import UploadButton from "components/Upload/UploadButton";
import { t } from "i18next";
import { Collection } from "types/collection";
import { UpdateSearch } from "types/search";
interface Iprops {
openSidebar: () => void;

View File

@@ -4,13 +4,13 @@ import { CustomHead } from "@/base/components/Head";
import { AppNavbar } from "@/base/components/Navbar";
import { setupI18n } from "@/base/i18n";
import log from "@/base/log";
import { runMigrations } from "@/new/photos/services/migrations";
import {
logStartupBanner,
logUnhandledErrorsAndRejections,
} from "@/base/log-web";
import { AppUpdate } from "@/base/types/ipc";
import DownloadManager from "@/new/photos/services/download";
import { runMigrations } from "@/new/photos/services/migrations";
import { initML, isMLSupported } from "@/new/photos/services/ml";
import { ensure } from "@/utils/ensure";
import { Overlay } from "@ente/shared/components/Container";

View File

@@ -7,6 +7,12 @@ import {
getLocalTrashedFiles,
} from "@/new/photos/services/files";
import { wipHasSwitchedOnceCmpAndSet } from "@/new/photos/services/ml";
import { search, setSearchableFiles } from "@/new/photos/services/search";
import {
SearchQuery,
SearchResultSummary,
UpdateSearch,
} from "@/new/photos/services/search/types";
import { EnteFile } from "@/new/photos/types/file";
import { mergeMetadata } from "@/new/photos/utils/file";
import { CenteredFlex } from "@ente/shared/components/Container";
@@ -31,7 +37,6 @@ import {
getKey,
} from "@ente/shared/storage/sessionStorage";
import type { User } from "@ente/shared/user/types";
import { isPromise } from "@ente/shared/utils";
import { Typography, styled } from "@mui/material";
import AuthenticateUserModal from "components/AuthenticateUserModal";
import Collections from "components/Collections";
@@ -106,7 +111,6 @@ import {
SetFilesDownloadProgressAttributes,
SetFilesDownloadProgressAttributesCreator,
} from "types/gallery";
import { Search, SearchResultSummary, UpdateSearch } from "types/search";
import { FamilyData } from "types/user";
import { checkSubscriptionPurchase } from "utils/billing";
import {
@@ -119,7 +123,6 @@ import {
hasNonSystemCollections,
splitNormalAndHiddenCollections,
} from "utils/collection";
import ComlinkSearchWorker from "utils/comlink/ComlinkSearchWorker";
import {
FILE_OPS_TYPE,
constructFileToCollectionMap,
@@ -193,7 +196,7 @@ export default function Gallery() {
const [collectionNamerAttributes, setCollectionNamerAttributes] =
useState<CollectionNamerAttributes>(null);
const [collectionNamerView, setCollectionNamerView] = useState(false);
const [search, setSearch] = useState<Search>(null);
const [searchQuery, setSearchQuery] = useState<SearchQuery>(null);
const [shouldDisableDropzone, setShouldDisableDropzone] = useState(false);
const [isPhotoSwipeOpen, setIsPhotoSwipeOpen] = useState(false);
// TODO(MR): This is never true currently, this is the WIP ability to show
@@ -400,13 +403,7 @@ export default function Gallery() {
};
}, []);
useEffectSingleThreaded(
async ([files]: [files: EnteFile[]]) => {
const searchWorker = await ComlinkSearchWorker.getInstance();
await searchWorker.setFiles(files);
},
[files],
);
useEffect(() => setSearchableFiles(files), [files]);
useEffect(() => {
if (!user || !files || !collections || !hiddenFiles || !trashedFiles) {
@@ -523,11 +520,9 @@ export default function Gallery() {
]);
}
const searchWorker = await ComlinkSearchWorker.getInstance();
let filteredFiles: EnteFile[] = [];
if (isInSearchMode) {
filteredFiles = getUniqueFiles(await searchWorker.search(search));
filteredFiles = getUniqueFiles(await search(searchQuery));
} else {
filteredFiles = getUniqueFiles(
(isInHiddenSection ? hiddenFiles : files).filter((item) => {
@@ -587,9 +582,9 @@ export default function Gallery() {
}),
);
}
if (search?.clip) {
if (searchQuery?.clip) {
return filteredFiles.sort((a, b) => {
return search.clip.get(b.id) - search.clip.get(a.id);
return searchQuery.clip.get(b.id) - searchQuery.clip.get(a.id);
});
}
const sortAsc = activeCollection?.pubMagicMetadata?.data?.asc ?? false;
@@ -605,7 +600,7 @@ export default function Gallery() {
tempDeletedFileIds,
tempHiddenFileIds,
hiddenFileIds,
search,
searchQuery,
activeCollectionID,
archivedCollections,
]);
@@ -975,7 +970,7 @@ export default function Gallery() {
if (newSearch?.collection) {
setActiveCollectionID(newSearch?.collection);
} else {
setSearch(newSearch);
setSearchQuery(newSearch);
}
setIsClipSearchResult(!!newSearch?.clip);
if (!newSearch?.collection) {
@@ -1243,37 +1238,6 @@ export default function Gallery() {
);
}
// useEffectSingleThreaded is a useEffect that will only run one at a time, and will
// caches the latest deps of requests that come in while it is running, and will
// run that after the current run is complete.
function useEffectSingleThreaded(
fn: (deps) => void | Promise<void>,
deps: any[],
): void {
const updateInProgress = useRef(false);
const nextRequestDepsRef = useRef<any[]>(null);
useEffect(() => {
const main = async (deps) => {
if (updateInProgress.current) {
nextRequestDepsRef.current = deps;
return;
}
updateInProgress.current = true;
const result = fn(deps);
if (isPromise(result)) {
await result;
}
updateInProgress.current = false;
if (nextRequestDepsRef.current) {
const deps = nextRequestDepsRef.current;
nextRequestDepsRef.current = null;
setTimeout(() => main(deps), 0);
}
};
main(deps);
}, deps);
}
/**
* Preload all three variants of a responsive image.
*/

View File

@@ -1,15 +1,5 @@
import log from "@/base/log";
import type { Location, LocationTagData } from "types/entity";
export interface City {
city: string;
country: string;
lat: number;
lng: number;
}
const DEFAULT_CITY_RADIUS = 10;
const KMS_PER_DEGREE = 111.16;
import type { City } from "@/new/photos/services/search/types";
class LocationSearchService {
private cities: Array<City> = [];
@@ -53,45 +43,3 @@ class LocationSearchService {
}
export default new LocationSearchService();
export function isInsideLocationTag(
location: Location,
locationTag: LocationTagData,
) {
return isLocationCloseToPoint(
location,
locationTag.centerPoint,
locationTag.radius,
);
}
export function isInsideCity(location: Location, city: City) {
return isLocationCloseToPoint(
{ latitude: city.lat, longitude: city.lng },
location,
DEFAULT_CITY_RADIUS,
);
}
function isLocationCloseToPoint(
centerPoint: Location,
location: Location,
radius: number,
) {
const a = (radius * _scaleFactor(centerPoint.latitude)) / KMS_PER_DEGREE;
const b = radius / KMS_PER_DEGREE;
const x = centerPoint.latitude - location.latitude;
const y = centerPoint.longitude - location.longitude;
if ((x * x) / (a * a) + (y * y) / (b * b) <= 1) {
return true;
}
return false;
}
///The area bounded by the location tag becomes more elliptical with increase
///in the magnitude of the latitude on the caritesian plane. When latitude is
///0 degrees, the ellipse is a circle with a = b = r. When latitude incrases,
///the major axis (a) has to be scaled by the secant of the latitude.
function _scaleFactor(lat: number) {
return 1 / Math.cos(lat * (Math.PI / 180));
}

View File

@@ -8,26 +8,27 @@ import {
mlStatusSnapshot,
wipSearchPersons,
} from "@/new/photos/services/ml";
import { parseDateComponents } from "@/new/photos/services/search";
import { parseDateComponents, search } from "@/new/photos/services/search";
import type {
SearchDateComponents,
SearchPerson,
} from "@/new/photos/services/search/types";
import {
City,
ClipSearchScores,
LocationTagData,
SearchOption,
SearchQuery,
Suggestion,
SuggestionType,
} from "@/new/photos/services/search/types";
import { EnteFile } from "@/new/photos/types/file";
import { t } from "i18next";
import { Collection } from "types/collection";
import { EntityType, LocationTag, LocationTagData } from "types/entity";
import {
ClipSearchScores,
Search,
SearchOption,
Suggestion,
SuggestionType,
} from "types/search";
import ComlinkSearchWorker from "utils/comlink/ComlinkSearchWorker";
import { EntityType, LocationTag } from "types/entity";
import { getUniqueFiles } from "utils/file";
import { getLatestEntities } from "./entityService";
import locationSearchService, { City } from "./locationSearchService";
import locationSearchService from "./locationSearchService";
export const getDefaultOptions = async () => {
return [
@@ -64,13 +65,10 @@ export const getAutoCompleteSuggestions =
async function convertSuggestionsToOptions(
suggestions: Suggestion[],
): Promise<SearchOption[]> {
const searchWorker = await ComlinkSearchWorker.getInstance();
const previewImageAppendedOptions: SearchOption[] = [];
for (const suggestion of suggestions) {
const searchQuery = convertSuggestionToSearchQuery(suggestion);
const resultFiles = getUniqueFiles(
await searchWorker.search(searchQuery),
);
const resultFiles = getUniqueFiles(await search(searchQuery));
if (searchQuery?.clip) {
resultFiles.sort((a, b) => {
const aScore = searchQuery.clip.get(a.id);
@@ -304,7 +302,7 @@ const searchClip = async (
return matches;
};
function convertSuggestionToSearchQuery(option: Suggestion): Search {
function convertSuggestionToSearchQuery(option: Suggestion): SearchQuery {
switch (option.type) {
case SuggestionType.DATE:
return {

View File

@@ -1,3 +1,5 @@
import type { LocationTagData } from "@/new/photos/services/search/types";
export enum EntityType {
LOCATION_TAG = "location",
}
@@ -25,19 +27,6 @@ export interface EncryptedEntity {
userID: number;
}
export interface Location {
latitude: number | null;
longitude: number | null;
}
export interface LocationTagData {
name: string;
radius: number;
aSquare: number;
bSquare: number;
centerPoint: Location;
}
export type LocationTag = Entity<LocationTagData>;
export interface Entity<T>

View File

@@ -1,65 +0,0 @@
import { FileType } from "@/media/file-type";
import type { MLStatus } from "@/new/photos/services/ml";
import type {
SearchDateComponents,
SearchPerson,
} from "@/new/photos/services/search/types";
import { EnteFile } from "@/new/photos/types/file";
import { City } from "services/locationSearchService";
import { LocationTagData } from "types/entity";
export enum SuggestionType {
DATE = "DATE",
LOCATION = "LOCATION",
COLLECTION = "COLLECTION",
FILE_NAME = "FILE_NAME",
PERSON = "PERSON",
INDEX_STATUS = "INDEX_STATUS",
FILE_CAPTION = "FILE_CAPTION",
FILE_TYPE = "FILE_TYPE",
CLIP = "CLIP",
CITY = "CITY",
}
export interface Suggestion {
type: SuggestionType;
label: string;
value:
| SearchDateComponents
| number[]
| SearchPerson
| MLStatus
| LocationTagData
| City
| FileType
| ClipSearchScores;
hide?: boolean;
}
export type Search = {
date?: SearchDateComponents;
location?: LocationTagData;
city?: City;
collection?: number;
files?: number[];
person?: SearchPerson;
fileType?: FileType;
clip?: ClipSearchScores;
};
export type SearchResultSummary = {
optionName: string;
fileCount: number;
};
export interface SearchOption extends Suggestion {
fileCount: number;
previewFiles: EnteFile[];
}
export type UpdateSearch = (
search: Search,
summary: SearchResultSummary,
) => void;
export type ClipSearchScores = Map<number, number>;

View File

@@ -1,32 +0,0 @@
import { haveWindow } from "@/base/env";
import { ComlinkWorker } from "@/base/worker/comlink-worker";
import type { Remote } from "comlink";
import { type DedicatedSearchWorker } from "worker/search.worker";
class ComlinkSearchWorker {
private comlinkWorkerInstance: Remote<DedicatedSearchWorker>;
private comlinkWorker: ComlinkWorker<typeof DedicatedSearchWorker>;
async getInstance() {
if (!this.comlinkWorkerInstance) {
if (!this.comlinkWorker)
this.comlinkWorker = getDedicatedSearchWorker();
this.comlinkWorkerInstance = await this.comlinkWorker.remote;
}
return this.comlinkWorkerInstance;
}
}
export const getDedicatedSearchWorker = () => {
if (haveWindow()) {
const cryptoComlinkWorker = new ComlinkWorker<
typeof DedicatedSearchWorker
>(
"ente-search-worker",
new Worker(new URL("worker/search.worker.ts", import.meta.url)),
);
return cryptoComlinkWorker;
}
};
export default new ComlinkSearchWorker();

View File

@@ -1,88 +0,0 @@
import { getUICreationDate } from "@/media/file-metadata";
import type { SearchDateComponents } from "@/new/photos/services/search/types";
import { EnteFile } from "@/new/photos/types/file";
import { getPublicMagicMetadataSync } from "@ente/shared/file-metadata";
import * as Comlink from "comlink";
import {
isInsideCity,
isInsideLocationTag,
} from "services/locationSearchService";
import { Search } from "types/search";
export class DedicatedSearchWorker {
private files: EnteFile[] = [];
setFiles(files: EnteFile[]) {
this.files = files;
}
search(search: Search) {
return this.files.filter((file) => {
return isSearchedFile(file, search);
});
}
}
Comlink.expose(DedicatedSearchWorker, self);
function isSearchedFile(file: EnteFile, search: Search) {
if (search?.collection) {
return search.collection === file.collectionID;
}
if (search?.date) {
return isDateComponentsMatch(
search.date,
getUICreationDate(file, getPublicMagicMetadataSync(file)),
);
}
if (search?.location) {
return isInsideLocationTag(
{
latitude: file.metadata.latitude,
longitude: file.metadata.longitude,
},
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;
}
if (search?.person) {
return search.person.files.indexOf(file.id) !== -1;
}
if (typeof search?.fileType !== "undefined") {
return search.fileType === file.metadata.fileType;
}
if (typeof search?.clip !== "undefined") {
return search.clip.has(file.id);
}
return false;
}
const isDateComponentsMatch = (
{ year, month, day, weekday, hour }: SearchDateComponents,
date: Date,
) => {
// Components are guaranteed to have at least one attribute present, so
// start by assuming true.
let match = true;
if (year) match = date.getFullYear() == year;
// JS getMonth is 0-indexed.
if (match && month) match = date.getMonth() + 1 == month;
if (match && day) match = date.getDate() == day;
if (match && weekday) match = date.getDay() == weekday;
if (match && hour) match = date.getHours() == hour;
return match;
};

View File

@@ -2,7 +2,44 @@ import { nullToUndefined } from "@/utils/transform";
import type { Component } from "chrono-node";
import * as chrono from "chrono-node";
import i18n, { t } from "i18next";
import type { SearchDateComponents } from "./types";
import type { SearchDateComponents, SearchQuery } from "./types";
import { ComlinkWorker } from "@/base/worker/comlink-worker";
import type { EnteFile } from "../../types/file";
import type { SearchWorker } from "./worker";
/**
* Cached instance of the {@link ComlinkWorker} that wraps our web worker.
*/
let _comlinkWorker: ComlinkWorker<typeof SearchWorker> | undefined;
/**
* Lazily created, cached, instance of {@link SearchWorker}.
*/
const worker = () => (_comlinkWorker ??= createComlinkWorker()).remote;
/**
* Create a new instance of a comlink worker that wraps a {@link SearchWorker}
* web worker.
*/
const createComlinkWorker = () =>
new ComlinkWorker<typeof SearchWorker>(
"search",
new Worker(new URL("worker.ts", import.meta.url)),
);
/**
* Set the files over which we will search.
*/
export const setSearchableFiles = (enteFiles: EnteFile[]) =>
void worker().then((w) => w.setEnteFiles(enteFiles));
/**
* Search for and return the list of {@link EnteFile}s that match the given
* {@link search} query.
*/
export const search = async (search: SearchQuery) =>
worker().then((w) => w.search(search));
interface DateSearchResult {
components: SearchDateComponents;

View File

@@ -1,9 +1,11 @@
/**
* @file types shared between the main thread interface to search (`index.ts`)
* and the search worker (`worker.ts`)
* and the search worker that does the actual searching (`worker.ts`).
*/
import type { EnteFile } from "../../types/file";
import { FileType } from "@/media/file-type";
import type { MLStatus } from "@/new/photos/services/ml";
import type { EnteFile } from "@/new/photos/types/file";
/**
* A parsed version of a potential natural language date time string.
@@ -48,3 +50,81 @@ export interface SearchPerson {
displayFaceID: string;
displayFaceFile: EnteFile;
}
// TODO-cgroup: Audit below
export interface Location {
latitude: number | null;
longitude: number | null;
}
export interface LocationTagData {
name: string;
radius: number;
aSquare: number;
bSquare: number;
centerPoint: Location;
}
export interface City {
city: string;
country: string;
lat: number;
lng: number;
}
export enum SuggestionType {
DATE = "DATE",
LOCATION = "LOCATION",
COLLECTION = "COLLECTION",
FILE_NAME = "FILE_NAME",
PERSON = "PERSON",
INDEX_STATUS = "INDEX_STATUS",
FILE_CAPTION = "FILE_CAPTION",
FILE_TYPE = "FILE_TYPE",
CLIP = "CLIP",
CITY = "CITY",
}
export interface Suggestion {
type: SuggestionType;
label: string;
value:
| SearchDateComponents
| number[]
| SearchPerson
| MLStatus
| LocationTagData
| City
| FileType
| ClipSearchScores;
hide?: boolean;
}
export interface SearchQuery {
date?: SearchDateComponents;
location?: LocationTagData;
city?: City;
collection?: number;
files?: number[];
person?: SearchPerson;
fileType?: FileType;
clip?: ClipSearchScores;
}
export interface SearchResultSummary {
optionName: string;
fileCount: number;
}
export interface SearchOption extends Suggestion {
fileCount: number;
previewFiles: EnteFile[];
}
export type UpdateSearch = (
search: SearchQuery,
summary: SearchResultSummary,
) => void;
export type ClipSearchScores = Map<number, number>;

View File

@@ -0,0 +1,146 @@
// TODO-cgroups
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/prefer-includes */
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
import { getUICreationDate } from "@/media/file-metadata";
import type {
City,
Location,
LocationTagData,
} from "@/new/photos/services/search/types";
import type { EnteFile } from "@/new/photos/types/file";
import { getPublicMagicMetadataSync } from "@ente/shared/file-metadata";
import { expose } from "comlink";
import type { SearchDateComponents, SearchQuery } from "./types";
/**
* A web worker that runs the search asynchronously so that the main thread
* remains responsive.
*/
export class SearchWorker {
private enteFiles: EnteFile[] = [];
/**
* Set the files that we should search across.
*/
setEnteFiles(enteFiles: EnteFile[]) {
this.enteFiles = enteFiles;
}
/**
* Return {@link EnteFile}s that satisfy the given {@link searchQuery}
* query.
*/
search(searchQuery: SearchQuery) {
return this.enteFiles.filter((f) => isMatch(f, searchQuery));
}
}
expose(SearchWorker);
function isMatch(file: EnteFile, query: SearchQuery) {
if (query?.collection) {
return query.collection === file.collectionID;
}
if (query?.date) {
return isDateComponentsMatch(
query.date,
getUICreationDate(file, getPublicMagicMetadataSync(file)),
);
}
if (query?.location) {
return isInsideLocationTag(
{
latitude: file.metadata.latitude ?? null,
longitude: file.metadata.longitude ?? null,
},
query.location,
);
}
if (query?.city) {
return isInsideCity(
{
latitude: file.metadata.latitude ?? null,
longitude: file.metadata.longitude ?? null,
},
query.city,
);
}
if (query?.files) {
return query.files.indexOf(file.id) !== -1;
}
if (query?.person) {
return query.person.files.indexOf(file.id) !== -1;
}
if (typeof query?.fileType !== "undefined") {
return query.fileType === file.metadata.fileType;
}
if (typeof query?.clip !== "undefined") {
return query.clip.has(file.id);
}
return false;
}
const isDateComponentsMatch = (
{ year, month, day, weekday, hour }: SearchDateComponents,
date: Date,
) => {
// Components are guaranteed to have at least one attribute present, so
// start by assuming true.
let match = true;
if (year) match = date.getFullYear() == year;
// JS getMonth is 0-indexed.
if (match && month) match = date.getMonth() + 1 == month;
if (match && day) match = date.getDate() == day;
if (match && weekday) match = date.getDay() == weekday;
if (match && hour) match = date.getHours() == hour;
return match;
};
export function isInsideLocationTag(
location: Location,
locationTag: LocationTagData,
) {
return isLocationCloseToPoint(
location,
locationTag.centerPoint,
locationTag.radius,
);
}
const DEFAULT_CITY_RADIUS = 10;
const KMS_PER_DEGREE = 111.16;
export function isInsideCity(location: Location, city: City) {
return isLocationCloseToPoint(
{ latitude: city.lat, longitude: city.lng },
location,
DEFAULT_CITY_RADIUS,
);
}
function isLocationCloseToPoint(
centerPoint: Location,
location: Location,
radius: number,
) {
const a = (radius * _scaleFactor(centerPoint.latitude!)) / KMS_PER_DEGREE;
const b = radius / KMS_PER_DEGREE;
const x = centerPoint.latitude! - location.latitude!;
const y = centerPoint.longitude! - location.longitude!;
if ((x * x) / (a * a) + (y * y) / (b * b) <= 1) {
return true;
}
return false;
}
///The area bounded by the location tag becomes more elliptical with increase
///in the magnitude of the latitude on the caritesian plane. When latitude is
///0 degrees, the ellipse is a circle with a = b = r. When latitude incrases,
///the major axis (a) has to be scaled by the secant of the latitude.
function _scaleFactor(lat: number) {
return 1 / Math.cos(lat * (Math.PI / 180));
}

View File

@@ -3,7 +3,7 @@ import {
decryptPublicMagicMetadata,
type PublicMagicMetadata,
} from "@/media/file-metadata";
import { EnteFile } from "@/new/photos/types/file";
import type { EnteFile } from "@/new/photos/types/file";
import { fileLogID } from "@/new/photos/utils/file";
/**