From fed667ccd88d45536e101d969fd525b05cff03cf Mon Sep 17 00:00:00 2001
From: jake <77554505+brxken128@users.noreply.github.com>
Date: Tue, 19 Sep 2023 09:46:14 +0100
Subject: [PATCH] [ENG-1097] DMS coordinate display support (#1335)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* offer DD and DMS coordinate displays
* clean up & optimise the conversion functions
* add support for changing between dd/dms (and some other example changeable formats)
* even further cleanup
* auto format
* dedicated clickable component to clean things up
* slim it down by passing platform directly
* make dist/temp settable, use dedicated format store
* use freedom units if locale is `en-US` 🦅
* rename the store and attempt to make it more typesafe
* cleanup mainly
* DD -> "Decimal" in the UI and swap the options as DMS will be the default
* remove useless schema
* only show S decimal on hover for DMS
* show `x` after zoom if it's a valid number
---
.../Explorer/Inspector/MediaData.tsx | 158 +++++++++++-------
.../$libraryId/Explorer/Inspector/index.tsx | 8 +-
.../$libraryId/settings/client/appearance.tsx | 52 +++++-
interface/app/onboarding/context.tsx | 9 +
packages/client/src/hooks/index.ts | 1 +
.../client/src/hooks/useUnitFormatStore.tsx | 23 +++
6 files changed, 183 insertions(+), 68 deletions(-)
create mode 100644 packages/client/src/hooks/useUnitFormatStore.tsx
diff --git a/interface/app/$libraryId/Explorer/Inspector/MediaData.tsx b/interface/app/$libraryId/Explorer/Inspector/MediaData.tsx
index b6dd1cfd4..2cf7f758c 100644
--- a/interface/app/$libraryId/Explorer/Inspector/MediaData.tsx
+++ b/interface/app/$libraryId/Explorer/Inspector/MediaData.tsx
@@ -1,23 +1,74 @@
-import { MediaLocation, MediaMetadata, MediaTime, Orientation } from '@sd/client';
-
+import {
+ CoordinatesFormat,
+ MediaLocation,
+ MediaMetadata,
+ MediaTime,
+ useUnitFormatStore
+} from '@sd/client';
import Accordion from '~/components/Accordion';
-import { usePlatform } from '~/util/Platform';
+import { Platform, usePlatform } from '~/util/Platform';
+
import { MetaData } from './index';
interface Props {
data: MediaMetadata;
}
-function formatMediaTime(loc: MediaTime): string | null {
- if (loc === 'Undefined') return null;
- if ('Utc' in loc) return loc.Utc;
- if ('Naive' in loc) return loc.Naive;
+const formatMediaTime = (time: MediaTime): string | null => {
+ if (time === 'Undefined') return null;
+ if ('Utc' in time) return time.Utc;
+ if ('Naive' in time) return time.Naive;
return null;
-}
+};
-function formatLocation(loc: MediaLocation, dp: number): string {
- return `${loc.latitude.toFixed(dp)}, ${loc.longitude.toFixed(dp)}`;
-}
+const formatLocationDD = (loc: MediaLocation, dp?: number): string => {
+ // the lack of a + here will mean that coordinates may have padding at the end
+ // google does the same (if one is larger than the other, the smaller one will be padded with zeroes)
+ return `${loc.latitude.toFixed(dp ?? 8)}, ${loc.longitude.toFixed(dp ?? 8)}`;
+};
+
+const formatLocationDMS = (loc: MediaLocation, dp?: number): string => {
+ const formatCoordinatesAsDMS = (
+ coordinates: number,
+ positiveChar: string,
+ negativeChar: string
+ ): string => {
+ const abs = getAbsoluteDecimals(coordinates);
+ const d = Math.trunc(coordinates);
+ const m = Math.trunc(60 * abs);
+ // adding 0.05 before rounding and truncating with `toFixed` makes it match up with google
+ const s = (abs * 3600 - m * 60 + 0.05).toFixed(dp ?? 1);
+ const sign = coordinates > 0 ? positiveChar : negativeChar;
+ return `${d}°${m}'${s}"${sign}`;
+ };
+
+ return `${formatCoordinatesAsDMS(loc.latitude, 'N', 'S')} ${formatCoordinatesAsDMS(
+ loc.longitude,
+ 'E',
+ 'W'
+ )}`;
+};
+
+const getAbsoluteDecimals = (num: number): number => {
+ const x = num.toString();
+ // this becomes +0.xxxxxxxxx and is needed to convert the minutes/seconds for DMS
+ return Math.abs(Number.parseFloat('0.' + x.substring(x.indexOf('.') + 1)));
+};
+
+const formatLocation = (loc: MediaLocation, format: CoordinatesFormat, dp?: number): string => {
+ return format === 'dd' ? formatLocationDD(loc, dp) : formatLocationDMS(loc, dp);
+};
+
+const UrlMetadataValue = (props: { text: string; url: string; platform: Platform }) => (
+ {
+ e.preventDefault();
+ props.platform.openLink(props.url);
+ }}
+ >
+ {props.text}
+
+);
const orientations = {
Normal: 'Normal',
@@ -30,8 +81,9 @@ const orientations = {
CW270: 'Rotated 270° clockwise'
};
-function MediaData({ data }: Props) {
+const MediaData = ({ data }: Props) => {
const platform = usePlatform();
+ const coordinatesFormat = useUnitFormatStore().coordinatesFormat;
return data.type === 'Image' ? (
@@ -40,76 +92,62 @@ function MediaData({ data }: Props) {
{
- e.preventDefault();
- if (data.location)
- platform.openLink(
- `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(
- data.location.latitude
- )}%2c${encodeURIComponent(data.location.longitude)}`
- );
- }}
- >
- {formatLocation(data.location, 3)}
-
- ) : (
- '--'
+ data.location && (
+
)
}
/>
{
- e.preventDefault();
- if (data.location)
- platform.openLink(
- `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(
- data.location.pluscode
- )}`
- );
- }}
- >
- {data.location?.pluscode}
-
- ) : (
- '--'
+ data.location?.pluscode && (
+
)
}
/>
- {data.dimensions.width} x {data.dimensions.height}
- >
- }
+ value={`${data.dimensions.width} x ${data.dimensions.height}`}
/>
-
+
-
+
) : null;
-}
+};
export default MediaData;
diff --git a/interface/app/$libraryId/Explorer/Inspector/index.tsx b/interface/app/$libraryId/Explorer/Inspector/index.tsx
index 4e45ce710..3f453f7e3 100644
--- a/interface/app/$libraryId/Explorer/Inspector/index.tsx
+++ b/interface/app/$libraryId/Explorer/Inspector/index.tsx
@@ -1,6 +1,3 @@
-import { Image, Image_Light } from '@sd/assets/icons';
-import clsx from 'clsx';
-import dayjs from 'dayjs';
import {
Barcode,
CircleWavyCheck,
@@ -15,6 +12,9 @@ import {
Path,
Snowflake
} from '@phosphor-icons/react';
+import { Image, Image_Light } from '@sd/assets/icons';
+import clsx from 'clsx';
+import dayjs from 'dayjs';
import {
forwardRef,
useCallback,
@@ -36,10 +36,10 @@ import {
type ExplorerItem
} from '@sd/client';
import { Button, Divider, DropdownMenu, Tooltip, tw } from '@sd/ui';
-
import AssignTagMenuItems from '~/components/AssignTagMenuItems';
import { useIsDark } from '~/hooks';
import { isNonEmpty } from '~/util';
+
import { useExplorerContext } from '../Context';
import { FileThumb } from '../FilePath/Thumb';
import { useQuickPreviewStore } from '../QuickPreview/store';
diff --git a/interface/app/$libraryId/settings/client/appearance.tsx b/interface/app/$libraryId/settings/client/appearance.tsx
index 2b291049a..2820c2a83 100644
--- a/interface/app/$libraryId/settings/client/appearance.tsx
+++ b/interface/app/$libraryId/settings/client/appearance.tsx
@@ -1,11 +1,18 @@
+import { CheckCircle } from '@phosphor-icons/react';
import clsx from 'clsx';
import { useMotionValueEvent, useScroll } from 'framer-motion';
-import { CheckCircle } from '@phosphor-icons/react';
import { useEffect, useRef, useState } from 'react';
-import { getThemeStore, Themes, useThemeStore, useZodForm } from '@sd/client';
-import { Button, Form, SwitchField, z } from '@sd/ui';
-
+import {
+ getThemeStore,
+ getUnitFormatStore,
+ Themes,
+ useThemeStore,
+ useUnitFormatStore,
+ useZodForm
+} from '@sd/client';
+import { Button, Divider, Form, Select, SelectOption, SwitchField, z } from '@sd/ui';
import { usePlatform } from '~/util/Platform';
+
import { Heading } from '../Layout';
import Setting from '../Setting';
@@ -56,6 +63,8 @@ const themes: Theme[] = [
export const Component = () => {
const { lockAppTheme } = usePlatform();
const themeStore = useThemeStore();
+ const formatStore = useUnitFormatStore();
+
const [selectedTheme, setSelectedTheme] = useState(
themeStore.syncThemeWithSystem === true ? 'system' : themeStore.theme
);
@@ -107,6 +116,7 @@ export const Component = () => {
document.documentElement.style.setProperty('--dark-hue', hue.toString());
}
};
+
return (
<>
+
+
+
Display Formats
+
+
+
+
+
+
+
+
+
+
+
+
+
>
);
};
diff --git a/interface/app/onboarding/context.tsx b/interface/app/onboarding/context.tsx
index ecea64b25..17326c52c 100644
--- a/interface/app/onboarding/context.tsx
+++ b/interface/app/onboarding/context.tsx
@@ -3,9 +3,12 @@ import { createContext, useContext } from 'react';
import { useNavigate } from 'react-router';
import {
currentLibraryCache,
+ DistanceFormat,
getOnboardingStore,
+ getUnitFormatStore,
resetOnboardingStore,
telemetryStore,
+ TemperatureFormat,
useBridgeMutation,
useCachedLibraries,
useMultiZodForm,
@@ -74,6 +77,12 @@ const useFormState = () => {
const queryClient = useQueryClient();
const submitPlausibleEvent = usePlausibleEvent();
+ if (window.navigator.language === 'en-US') {
+ // not perfect as some linux users use en-US by default, same w/ windows
+ getUnitFormatStore().distanceFormat = 'miles';
+ getUnitFormatStore().temperatureFormat = 'fahrenheit';
+ }
+
const createLibrary = useBridgeMutation('library.create');
const submit = handleSubmit(
diff --git a/packages/client/src/hooks/index.ts b/packages/client/src/hooks/index.ts
index 73bfd6a3d..2c75cfdc1 100644
--- a/packages/client/src/hooks/index.ts
+++ b/packages/client/src/hooks/index.ts
@@ -10,3 +10,4 @@ export * from './useTelemetryState';
export * from './useThemeStore';
export * from './useNotifications';
export * from './useForceUpdate';
+export * from './useUnitFormatStore';
diff --git a/packages/client/src/hooks/useUnitFormatStore.tsx b/packages/client/src/hooks/useUnitFormatStore.tsx
new file mode 100644
index 000000000..11b9e7c99
--- /dev/null
+++ b/packages/client/src/hooks/useUnitFormatStore.tsx
@@ -0,0 +1,23 @@
+import { useSnapshot } from 'valtio';
+
+import { valtioPersist } from '../lib';
+
+export type CoordinatesFormat = 'dms' | 'dd';
+export type DistanceFormat = 'km' | 'miles';
+export type TemperatureFormat = 'celsius' | 'fahrenheit';
+
+const unitFormatStore = valtioPersist('sd-display-units', {
+ // these are the defaults as 99% of users would want to see them this way
+ // if the `en-US` locale is detected during onboarding, the distance/temp are changed to freedom units
+ coordinatesFormat: 'dms' as CoordinatesFormat,
+ distanceFormat: 'km' as DistanceFormat,
+ temperatureFormat: 'celsius' as TemperatureFormat
+});
+
+export function useUnitFormatStore() {
+ return useSnapshot(unitFormatStore);
+}
+
+export function getUnitFormatStore() {
+ return unitFormatStore;
+}