[ENG-1097] DMS coordinate display support (#1335)

* 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
This commit is contained in:
jake 2023-09-19 09:46:14 +01:00 committed by GitHub
parent e847f2e0f4
commit 0382a4e48f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 183 additions and 68 deletions

View file

@ -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 }) => (
<a
onClick={(e) => {
e.preventDefault();
props.platform.openLink(props.url);
}}
>
{props.text}
</a>
);
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' ? (
<div className="flex flex-col gap-0 py-2">
@ -40,76 +92,62 @@ function MediaData({ data }: Props) {
<MetaData label="Type" value={data.type} />
<MetaData
label="Location"
tooltipValue={
data.location
? `${data.location.latitude}, ${data.location.longitude}`
: '--'
}
tooltipValue={data.location && formatLocation(data.location, coordinatesFormat)}
value={
data.location ? (
<a
onClick={(e) => {
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)}
</a>
) : (
'--'
data.location && (
<UrlMetadataValue
url={`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(
formatLocation(data.location, 'dd')
)}`}
text={formatLocation(
data.location,
coordinatesFormat,
coordinatesFormat === 'dd' ? 4 : 0
)}
platform={platform}
/>
)
}
/>
<MetaData
label="Plus Code"
value={
data.location?.pluscode ? (
<a
onClick={(e) => {
e.preventDefault();
if (data.location)
platform.openLink(
`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(
data.location.pluscode
)}`
);
}}
>
{data.location?.pluscode}
</a>
) : (
'--'
data.location?.pluscode && (
<UrlMetadataValue
url={`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(
data.location.pluscode
)}`}
text={data.location.pluscode}
platform={platform}
/>
)
}
/>
<MetaData
label="Dimensions"
value={
<>
{data.dimensions.width} x {data.dimensions.height}
</>
}
value={`${data.dimensions.width} x ${data.dimensions.height}`}
/>
<MetaData label="Device" value={data.camera_data.device_make} />
<MetaData label="Model" value={data.camera_data.device_model} />
<MetaData
label="Orientation"
value={orientations[data.camera_data.orientation] ?? '--'}
/>
<MetaData label="Orientation" value={orientations[data.camera_data.orientation]} />
<MetaData label="Color profile" value={data.camera_data.color_profile} />
<MetaData label="Color space" value={data.camera_data.color_space} />
<MetaData label="Flash" value={data.camera_data.flash?.mode} />
<MetaData label="Zoom" value={data.camera_data.zoom?.toFixed(2)} />
<MetaData
label="Zoom"
value={
data.camera_data &&
data.camera_data.zoom &&
!Number.isNaN(data.camera_data.zoom)
? `${data.camera_data.zoom.toFixed(2) + 'x'}`
: '--'
}
/>
<MetaData label="Iso" value={data.camera_data.iso} />
<MetaData label="Software" value={data.camera_data.software} />
</Accordion>
</div>
) : null;
}
};
export default MediaData;

View file

@ -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';

View file

@ -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<Theme['themeValue']>(
themeStore.syncThemeWithSystem === true ? 'system' : themeStore.theme
);
@ -107,6 +116,7 @@ export const Component = () => {
document.documentElement.style.setProperty('--dark-hue', hue.toString());
}
};
return (
<>
<Form form={form} onSubmit={onSubmit}>
@ -212,6 +222,40 @@ export const Component = () => {
</Setting>
</div>
</Form>
<Divider />
<div className="flex flex-col gap-4">
<h1 className="mb-3 text-lg font-bold text-ink">Display Formats</h1>
<Setting mini title="Coordinates">
<Select
onChange={(e) => (getUnitFormatStore().coordinatesFormat = e)}
value={formatStore.coordinatesFormat}
>
<SelectOption value="dms">DMS</SelectOption>
<SelectOption value="dd">Decimal</SelectOption>
</Select>
</Setting>
<Setting mini title="Distance">
<Select
onChange={(e) => (getUnitFormatStore().distanceFormat = e)}
value={formatStore.distanceFormat}
>
<SelectOption value="km">Kilometers</SelectOption>
<SelectOption value="miles">Miles</SelectOption>
</Select>
</Setting>
<Setting mini title="Temperature">
<Select
onChange={(e) => (getUnitFormatStore().temperatureFormat = e)}
value={formatStore.temperatureFormat}
>
<SelectOption value="celsius">Celsius</SelectOption>
<SelectOption value="fahrenheit">Fahrenheit</SelectOption>
</Select>
</Setting>
</div>
</>
);
};

View file

@ -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(

View file

@ -10,3 +10,4 @@ export * from './useTelemetryState';
export * from './useThemeStore';
export * from './useNotifications';
export * from './useForceUpdate';
export * from './useUnitFormatStore';

View file

@ -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;
}