mirror of
https://github.com/cupcakearmy/gps-info.git
synced 2025-09-05 22:20:38 +00:00
first version
This commit is contained in:
39
src/app.css
Normal file
39
src/app.css
Normal file
@@ -0,0 +1,39 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
html,
|
||||
body {
|
||||
font-family: 'Space Mono', monospace;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: var(--clr-bg-0);
|
||||
color: var(--clr-text-0);
|
||||
}
|
||||
|
||||
body {
|
||||
padding-top: env(safe-area-inset-top);
|
||||
padding-right: env(safe-area-inset-right);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
padding-left: env(safe-area-inset-left);
|
||||
}
|
||||
|
||||
:root {
|
||||
--clr-bg-0: #180b2e;
|
||||
--clr-bg-1: #006ba6;
|
||||
--clr-text-0: #fcfcfc;
|
||||
|
||||
--radius: 2rem;
|
||||
--anim: all ease-in-out 200ms;
|
||||
}
|
||||
|
||||
* {
|
||||
scroll-behavior: smooth;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
svg {
|
||||
height: 1em;
|
||||
display: inline-block;
|
||||
aspect-ratio: 1/1;
|
||||
}
|
9
src/app.d.ts
vendored
Normal file
9
src/app.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
// and what to do when importing types
|
||||
declare namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface Platform {}
|
||||
}
|
16
src/app.html
Normal file
16
src/app.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, viewport-fit=cover, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
253
src/fonts.css
Normal file
253
src/fonts.css
Normal file
@@ -0,0 +1,253 @@
|
||||
/* Space Mono */
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Space Mono';
|
||||
src: url(/fonts/2b80d9191857114935a7ddc8651898ce5c332b4b.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Space Mono';
|
||||
src: url(/fonts/81d14acb9ab7a87e474981a7eddf71ae203f9b7e.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Space Mono';
|
||||
src: url(/fonts/886749ee9261d1c21938d2fc8c6ec6c3b5c3576d.woff2) format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Space Mono';
|
||||
src: url(/fonts/3a2b22ae668071f27b7ad2bd25942f37d4336b5e.woff2) format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Space Mono';
|
||||
src: url(/fonts/6393c1457c4d218401f198659ab88ed244671302.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Space Mono';
|
||||
src: url(/fonts/f5f1a3e9a8759b88f30cafc1802d64e7843991de.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Space Mono';
|
||||
src: url(/fonts/a435ca6d3f780680052459c8e4dda1a2ae6097e2.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Space Mono';
|
||||
src: url(/fonts/da609c784b93e13caf638d1f12e37fba790511a7.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Space Mono';
|
||||
src: url(/fonts/31dffb3a079789ed54432fee3bff723f92b36737.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Space Mono';
|
||||
src: url(/fonts/988889b2ffccfc81e6a639d6ce2509b9dc5ffa0c.woff2) format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Space Mono';
|
||||
src: url(/fonts/6871557a261b4bd326a12b1989b91c4b86896591.woff2) format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Space Mono';
|
||||
src: url(/fonts/8fdccb510832b50cf7c29969c65cf634b6eecce3.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Space Mono';
|
||||
src: url(/fonts/3a1d609e542107f59aff6b69fb94284b994dfdcb.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Space Mono';
|
||||
src: url(/fonts/862757cd58804ddd5677c3698cc6f660aae08958.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Space Mono';
|
||||
src: url(/fonts/32f2ef7b1afb3cc954f8b24f74c4e43c2ca0949c.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
font-display: swap;
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Space Mono';
|
||||
src: url(/fonts/3d7ef8c205a8330e6e26451051c6a5ec269a0de9.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
font-display: swap;
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Space Mono';
|
||||
src: url(/fonts/d5158e1297edbc66f3444d7389cea3ee64820094.woff2) format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
font-display: swap;
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Space Mono';
|
||||
src: url(/fonts/2556c5d60452a40ff26729c27365e06e1fc4ba85.woff2) format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
font-display: swap;
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Space Mono';
|
||||
src: url(/fonts/1b0a405616130035d273abf57772350c7fae36a5.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
font-display: swap;
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Space Mono';
|
||||
src: url(/fonts/6100259de9ebd81a32ebc0cc609e8553e0f133fa.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
font-display: swap;
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Space Mono';
|
||||
src: url(/fonts/7513f6e9ca633ce8ab2b87e45c14853af063936d.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
font-display: swap;
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Space Mono';
|
||||
src: url(/fonts/a69f031a7a3da4af8283ad9ad2fd1bedd8d702b9.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
font-display: swap;
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Space Mono';
|
||||
src: url(/fonts/4e7e2952e005602594aa39de76082c4616a1a1f6.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
font-display: swap;
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Space Mono';
|
||||
src: url(/fonts/404a7d631e3ce4d7f23e759e6161fe4dd413ed22.woff2) format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
font-display: swap;
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Space Mono';
|
||||
src: url(/fonts/15e2e8e1f722cfbf268b579a29dc584bb2997351.woff2) format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
font-display: swap;
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Space Mono';
|
||||
src: url(/fonts/eff94ba10e75966d058a84922705892d3532b5dc.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
font-display: swap;
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Space Mono';
|
||||
src: url(/fonts/2601cce7273ff6fd23fd00ea90e17b67590faf44.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
font-display: swap;
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Space Mono';
|
||||
src: url(/fonts/0109c765e1b71c2254d207d41ada46e094d77c00.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
font-display: swap;
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
}
|
14
src/lib/components/Button.svelte
Normal file
14
src/lib/components/Button.svelte
Normal file
@@ -0,0 +1,14 @@
|
||||
<button class="px-4 py-1 text-sm">
|
||||
<slot />
|
||||
</button>
|
||||
|
||||
<style>
|
||||
button {
|
||||
display: block;
|
||||
border-radius: calc(var(--radius) / 2);
|
||||
border: 2px solid var(--clr-bg-1);
|
||||
background: hsla(0, 0%, 100%, 0.1);
|
||||
box-shadow: 0 0 24px -12px #ffffff36;
|
||||
width: max-content;
|
||||
}
|
||||
</style>
|
12
src/lib/components/Card.svelte
Normal file
12
src/lib/components/Card.svelte
Normal file
@@ -0,0 +1,12 @@
|
||||
<div class="px-6 py-4 w-full overflow-hidden">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div {
|
||||
border-radius: var(--radius);
|
||||
border: 2px solid var(--clr-bg-1);
|
||||
background: hsla(0, 0%, 100%, 0.1);
|
||||
box-shadow: 0 0 24px -8px #ffffff36;
|
||||
}
|
||||
</style>
|
5
src/lib/components/H1.svelte
Normal file
5
src/lib/components/H1.svelte
Normal file
@@ -0,0 +1,5 @@
|
||||
<script lang="ts">
|
||||
export let text: string
|
||||
</script>
|
||||
|
||||
<h1 class="text-center text-5xl mb-8">{text}</h1>
|
21
src/lib/components/IconCopy.svelte
Normal file
21
src/lib/components/IconCopy.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"
|
||||
><title>Copy</title><rect
|
||||
x="128"
|
||||
y="128"
|
||||
width="336"
|
||||
height="336"
|
||||
rx="57"
|
||||
ry="57"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="32"
|
||||
/><path
|
||||
d="M383.5 128l.5-24a56.16 56.16 0 00-56-56H112a64.19 64.19 0 00-64 64v216a56.16 56.16 0 0056 56h24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="32"
|
||||
/></svg
|
||||
>
|
After Width: | Height: | Size: 524 B |
10
src/lib/components/IconNavigate.svelte
Normal file
10
src/lib/components/IconNavigate.svelte
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"
|
||||
><title>Navigate</title><path
|
||||
d="M448 64L64 240.14h200a8 8 0 018 8V448z"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="32"
|
||||
/></svg
|
||||
>
|
After Width: | Height: | Size: 288 B |
19
src/lib/components/Measurement.svelte
Normal file
19
src/lib/components/Measurement.svelte
Normal file
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
export let label: string
|
||||
export let value: string | undefined
|
||||
export let unit: string | null = null
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="text-sm opacity-80">{label}</div>
|
||||
<div class="text-base">
|
||||
{#if value === undefined}
|
||||
No data
|
||||
{:else}
|
||||
{value}
|
||||
{/if}
|
||||
{#if unit && value !== undefined}
|
||||
<span class="text-xs -ml-1">{unit}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
16
src/lib/components/Needle.svelte
Normal file
16
src/lib/components/Needle.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { location } from '$lib/stores/location'
|
||||
import IconNavigate from './IconNavigate.svelte'
|
||||
|
||||
$: degree = ($location?.heading?.heading || 0) - 45
|
||||
</script>
|
||||
|
||||
<div class="text-xl" style="transform: rotate({degree % 360}deg);">
|
||||
<IconNavigate />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div {
|
||||
transition: var(--anim);
|
||||
}
|
||||
</style>
|
21
src/lib/components/Select.svelte
Normal file
21
src/lib/components/Select.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
export let value: string
|
||||
export let values: { label: string; value: string }[]
|
||||
</script>
|
||||
|
||||
<select bind:value class="px-4 py-1 text-sm">
|
||||
{#each values as { label, value }}
|
||||
<option {value}>{label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
|
||||
<style>
|
||||
select {
|
||||
display: block;
|
||||
border-radius: calc(var(--radius) / 2);
|
||||
border: 2px solid var(--clr-bg-1);
|
||||
background: hsla(0, 0%, 100%, 0.1);
|
||||
box-shadow: 0 0 24px -12px #ffffff36;
|
||||
width: max-content;
|
||||
}
|
||||
</style>
|
49
src/lib/components/Toggle.svelte
Normal file
49
src/lib/components/Toggle.svelte
Normal file
@@ -0,0 +1,49 @@
|
||||
<script lang="ts">
|
||||
export let value: boolean
|
||||
</script>
|
||||
|
||||
<label class="switch">
|
||||
<input type="checkbox" bind:checked={value} />
|
||||
<span class="slider" />
|
||||
</label>
|
||||
|
||||
<style>
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 3rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border-radius: var(--radius);
|
||||
border: 2px solid var(--clr-bg-1);
|
||||
background-color: var(--clr-bg-0);
|
||||
transition: var(--anim);
|
||||
}
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
height: calc(100% - 0.25rem);
|
||||
aspect-ratio: 1/1;
|
||||
left: 0.125rem;
|
||||
bottom: 0.125rem;
|
||||
background-color: var(--clr-bg-1);
|
||||
transition: var(--anim);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(calc(1.5rem));
|
||||
}
|
||||
</style>
|
6
src/lib/components/VerticalGrid.svelte
Normal file
6
src/lib/components/VerticalGrid.svelte
Normal file
@@ -0,0 +1,6 @@
|
||||
<script lang="ts">
|
||||
</script>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<slot />
|
||||
</div>
|
88
src/lib/geo.ts
Normal file
88
src/lib/geo.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
export enum CoordinateType {
|
||||
Decimal = 'DECIMAL',
|
||||
DMS = 'DMS',
|
||||
}
|
||||
|
||||
export enum CoordinateLink {
|
||||
GoogleMaps = 'GOOGLE_MAPS',
|
||||
OpenStreetMap = 'OpenStreetMap',
|
||||
}
|
||||
|
||||
export class Coordinates {
|
||||
constructor(private lat: number, private lon: number) {}
|
||||
|
||||
private static format(value: number, type: CoordinateType): string {
|
||||
switch (type) {
|
||||
case CoordinateType.DMS: {
|
||||
// https://en.wikipedia.org/wiki/Decimal_degrees#Example
|
||||
const degree = Math.trunc(value)
|
||||
const remainder = Math.abs(value - degree)
|
||||
const minute = Math.trunc(60 * remainder)
|
||||
const second = 3600 * remainder - 60 * minute
|
||||
|
||||
const d = degree.toFixed(0).padStart(3, '0')
|
||||
const m = minute.toFixed(0).padStart(2, '0')
|
||||
const s = second.toFixed(6).padStart(2, '0')
|
||||
return `${d}°${m}’${s}”`
|
||||
}
|
||||
case CoordinateType.Decimal: {
|
||||
return value.toFixed(7)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
format(type: CoordinateType, bearing: boolean): { lat: string; lon: string } {
|
||||
const lat = Coordinates.format(this.lat, type)
|
||||
const lon = Coordinates.format(this.lon, type)
|
||||
|
||||
if (bearing) {
|
||||
return {
|
||||
lat: `${lat.replace('-', '')} ${this.lat < 0 ? 'S' : 'N'}`,
|
||||
lon: `${lon.replace('-', '')} ${this.lon < 0 ? 'W' : 'E'}`,
|
||||
}
|
||||
}
|
||||
|
||||
return { lat, lon }
|
||||
}
|
||||
|
||||
link(type: CoordinateLink): string {
|
||||
switch (type) {
|
||||
case CoordinateLink.GoogleMaps:
|
||||
return `https://www.google.com/maps/place/${this.lat},${this.lon}`
|
||||
case CoordinateLink.OpenStreetMap:
|
||||
return `https://www.openstreetmap.org/?mlat=${this.lat}&mlon=${this.lon}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Heading {
|
||||
private static Bearings = {
|
||||
N: 0,
|
||||
NE: 45,
|
||||
E: 90,
|
||||
SE: 135,
|
||||
S: 180,
|
||||
SW: 225,
|
||||
W: 270,
|
||||
NW: 315,
|
||||
}
|
||||
|
||||
constructor(public heading: number) {}
|
||||
|
||||
format(): string {
|
||||
return `${this.heading.toFixed(0)}° ${this.bearing()}`
|
||||
}
|
||||
|
||||
bearing(): string {
|
||||
let bearing = ''
|
||||
let min = 360
|
||||
for (const [b, degree] of Object.entries(Heading.Bearings)) {
|
||||
const diff = Math.abs(this.heading - degree)
|
||||
if (diff < min) {
|
||||
min = diff
|
||||
bearing = b
|
||||
}
|
||||
}
|
||||
return bearing
|
||||
}
|
||||
}
|
6
src/lib/stores/app.ts
Normal file
6
src/lib/stores/app.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { App, type AppInfo } from '@capacitor/app'
|
||||
import { readable } from 'svelte/store'
|
||||
|
||||
export const app = readable<AppInfo | null>(null, (set) => {
|
||||
App.getInfo().then((info) => set(info))
|
||||
})
|
49
src/lib/stores/location.ts
Normal file
49
src/lib/stores/location.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Coordinates, Heading } from '$lib/geo'
|
||||
import { Geolocation } from '@capacitor/geolocation'
|
||||
import { readable } from 'svelte/store'
|
||||
|
||||
type NullableNumber = number | null
|
||||
type Location = {
|
||||
coords: Coordinates
|
||||
altitude: NullableNumber
|
||||
accuracy: {
|
||||
coords: NullableNumber
|
||||
altitude: NullableNumber
|
||||
}
|
||||
speed: NullableNumber
|
||||
heading: Heading | null
|
||||
time: {
|
||||
stamp: Date
|
||||
fix: number
|
||||
}
|
||||
}
|
||||
|
||||
let last = Date.now()
|
||||
|
||||
export const location = readable<Location | null>(null, (set) => {
|
||||
let id: string
|
||||
Geolocation.watchPosition({ enableHighAccuracy: true }, (p) => {
|
||||
if (p) {
|
||||
const now = Date.now()
|
||||
const location: Location = {
|
||||
coords: new Coordinates(p.coords.latitude, p.coords.longitude),
|
||||
altitude: p.coords.altitude,
|
||||
accuracy: {
|
||||
altitude: p.coords.altitudeAccuracy ?? null,
|
||||
coords: p.coords.accuracy,
|
||||
},
|
||||
speed: p.coords.speed,
|
||||
heading: p.coords.heading ? new Heading(p.coords.heading) : null,
|
||||
time: {
|
||||
stamp: new Date(p.timestamp),
|
||||
fix: now - last,
|
||||
},
|
||||
}
|
||||
last = now
|
||||
set(location)
|
||||
}
|
||||
}).then((i) => (id = i))
|
||||
return () => {
|
||||
if (id) Geolocation.clearWatch({ id })
|
||||
}
|
||||
})
|
30
src/lib/stores/settings.ts
Normal file
30
src/lib/stores/settings.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { CoordinateType } from '$lib/geo'
|
||||
import { Preferences } from '@capacitor/preferences'
|
||||
import { writable } from 'svelte/store'
|
||||
|
||||
export enum Unit {
|
||||
Metric = 'METRIC',
|
||||
Imperial = 'IMPERIAL',
|
||||
}
|
||||
|
||||
const defaults = {
|
||||
bearings: true,
|
||||
type: CoordinateType.DMS,
|
||||
unit: Unit.Metric,
|
||||
}
|
||||
type Defaults = typeof defaults
|
||||
|
||||
export function setting<K extends keyof Defaults>(key: K) {
|
||||
const store = writable<Defaults[K]>(defaults[key])
|
||||
Preferences.get({ key }).then(({ value }) => {
|
||||
store.set(value ? JSON.parse(value) : defaults[key])
|
||||
store.subscribe((value) => {
|
||||
Preferences.set({ key, value: JSON.stringify(value) })
|
||||
})
|
||||
})
|
||||
return store
|
||||
}
|
||||
|
||||
export const type = setting('type')
|
||||
export const bearings = setting('bearings')
|
||||
export const unit = setting('unit')
|
98
src/lib/views/Home.svelte
Normal file
98
src/lib/views/Home.svelte
Normal file
@@ -0,0 +1,98 @@
|
||||
<script lang="ts">
|
||||
import Button from '$lib/components/Button.svelte'
|
||||
import Card from '$lib/components/Card.svelte'
|
||||
import H1 from '$lib/components/H1.svelte'
|
||||
import Measurement from '$lib/components/Measurement.svelte'
|
||||
import Needle from '$lib/components/Needle.svelte'
|
||||
import VerticalGrid from '$lib/components/VerticalGrid.svelte'
|
||||
import { CoordinateLink } from '$lib/geo'
|
||||
import { app } from '$lib/stores/app'
|
||||
import { location } from '$lib/stores/location'
|
||||
import { bearings, type } from '$lib/stores/settings'
|
||||
import Settings from './Settings.svelte'
|
||||
|
||||
function pad(value: number): string {
|
||||
return value.toString().padStart(2, '0')
|
||||
}
|
||||
|
||||
$: stamp = $location?.time.stamp
|
||||
$: date = stamp && `${pad(stamp.getHours())}:${pad(stamp.getMinutes())}:${pad(stamp.getSeconds())}`
|
||||
|
||||
$: coords = $location?.coords.format($type, $bearings)
|
||||
$: fix = $location?.time.fix.toFixed(0)
|
||||
</script>
|
||||
|
||||
<H1 text="GPS Info" />
|
||||
|
||||
<VerticalGrid>
|
||||
<Card>
|
||||
<VerticalGrid>
|
||||
<div class="flex items-center">
|
||||
<div class="flex-grow">
|
||||
<Measurement label="Heading" value={$location?.heading?.format()} />
|
||||
</div>
|
||||
<Needle />
|
||||
</div>
|
||||
<Measurement label="Latitude" value={coords?.lat} />
|
||||
<Measurement label="Longitude" value={coords?.lon} />
|
||||
|
||||
<div>
|
||||
<span class="text-sm italic">Open in:</span>
|
||||
<div class="mt-1 grid gap-2 grid-flow-col auto-cols-max overflow-auto">
|
||||
<a href={$location?.coords.link(CoordinateLink.GoogleMaps)} target="_blank" rel="noopener noreferrer">
|
||||
<Button>Google Maps</Button>
|
||||
</a>
|
||||
<a href={$location?.coords.link(CoordinateLink.OpenStreetMap)} target="_blank" rel="noopener noreferrer">
|
||||
<Button>OpenStreetMap</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</VerticalGrid>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<VerticalGrid>
|
||||
<div class="grid gap-4 grid-cols-2">
|
||||
<Measurement label="Altitude" value={$location?.altitude?.toFixed(2)} unit="m.a.s.l." />
|
||||
<Measurement label="Speed" value={$location?.speed?.toFixed(2)} unit="m/s" />
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-sm italic">Accuracy</span>
|
||||
<div class="grid gap-4 grid-cols-2">
|
||||
<Measurement label="Vertical" value={$location?.accuracy.altitude?.toFixed(1)} unit="m" />
|
||||
<Measurement label="Horizontal" value={$location?.accuracy.coords?.toFixed(1)} unit="m" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-sm italic">Fixing</span>
|
||||
<div class="grid gap-4 grid-cols-2">
|
||||
<Measurement label="Time" value={fix} unit="mnarrows" />
|
||||
<Measurement label="Last" value={date || undefined} />
|
||||
</div>
|
||||
</div>
|
||||
</VerticalGrid>
|
||||
</Card>
|
||||
|
||||
<Settings />
|
||||
|
||||
<Card>
|
||||
<VerticalGrid>
|
||||
<span class="text-sm italic">About</span>
|
||||
<p>
|
||||
This is Open Source software.
|
||||
<a class="italic" href="https://github.com/cupcakearmy/gps-info" target="_blank" rel="noopener noreferrer"
|
||||
>👉 Source code</a
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
If you have issues / suggestions feel free to report them <a
|
||||
class="italic"
|
||||
href="https://github.com/cupcakearmy/gps-info/issues"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">here</a
|
||||
>.
|
||||
</p>
|
||||
<p>Version: {$app?.version}</p>
|
||||
</VerticalGrid>
|
||||
</Card>
|
||||
</VerticalGrid>
|
24
src/lib/views/Settings.svelte
Normal file
24
src/lib/views/Settings.svelte
Normal file
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import Card from '$lib/components/Card.svelte'
|
||||
import Select from '$lib/components/Select.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import VerticalGrid from '$lib/components/VerticalGrid.svelte'
|
||||
import { CoordinateType } from '$lib/geo'
|
||||
import { bearings, type } from '$lib/stores/settings'
|
||||
</script>
|
||||
|
||||
<Card>
|
||||
<span class="text-sm italic">Settings</span>
|
||||
<VerticalGrid>
|
||||
<div class="flex items-center">
|
||||
<div class="flex-auto">GPS format</div>
|
||||
<div>
|
||||
<Select bind:value={$type} values={Object.values(CoordinateType).map((v) => ({ label: v, value: v }))} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="flex-auto">Use bearings</div>
|
||||
<Toggle bind:value={$bearings} />
|
||||
</div>
|
||||
</VerticalGrid>
|
||||
</Card>
|
8
src/routes/+layout.svelte
Normal file
8
src/routes/+layout.svelte
Normal file
@@ -0,0 +1,8 @@
|
||||
<script lang="ts">
|
||||
import '../app.css'
|
||||
import '../fonts.css'
|
||||
</script>
|
||||
|
||||
<div class="px-8 py-16">
|
||||
<slot />
|
||||
</div>
|
1
src/routes/+layout.ts
Normal file
1
src/routes/+layout.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const ssr = false
|
5
src/routes/+page.svelte
Normal file
5
src/routes/+page.svelte
Normal file
@@ -0,0 +1,5 @@
|
||||
<script lang="ts">
|
||||
import Home from '$lib/views/Home.svelte'
|
||||
</script>
|
||||
|
||||
<Home />
|
Reference in New Issue
Block a user