mirror of
https://github.com/garraflavatra/gpstool.git
synced 2025-01-18 03:37:58 +00:00
Initial commit
This commit is contained in:
commit
5a39fec5ad
BIN
.github/screenshot.png
vendored
Normal file
BIN
.github/screenshot.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 726 KiB |
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
19
LICENSE.md
Normal file
19
LICENSE.md
Normal file
@ -0,0 +1,19 @@
|
||||
Copyright (c) 2023 Romein van Buren <<romein@vburen.nl>>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
18
README.md
Normal file
18
README.md
Normal file
@ -0,0 +1,18 @@
|
||||
# gpstool
|
||||
|
||||
A web-based tool for retrieving and converting GPS coordinates.
|
||||
|
||||
![Screenshot](./.github/screenshot.png)
|
||||
|
||||
## Local development
|
||||
|
||||
gpstool is developed using [Vite](https://vitejs.dev/) and [Svelte](https://svelte.dev/).
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Please see the [`LICENSE.md` file](./LICENSE.md) for the license.
|
18
index.html
Normal file
18
index.html
Normal file
@ -0,0 +1,18 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>gpstool</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<noscript>
|
||||
Please enable JavaScript in your browser to use gpstool.
|
||||
</noscript>
|
||||
</div>
|
||||
<script type="module" src="/src/app.js"></script>
|
||||
</body>
|
||||
</html>
|
32
jsconfig.json
Normal file
32
jsconfig.json
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "bundler",
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
/**
|
||||
* svelte-preprocess cannot figure out whether you have
|
||||
* a value or a type, so tell TypeScript to enforce using
|
||||
* `import type` instead of `import` for Types.
|
||||
*/
|
||||
"verbatimModuleSyntax": true,
|
||||
"isolatedModules": true,
|
||||
"resolveJsonModule": true,
|
||||
/**
|
||||
* To have warnings / errors of the Svelte compiler at the
|
||||
* correct position, enable source maps by default.
|
||||
*/
|
||||
"sourceMap": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
/**
|
||||
* Typecheck JS in `.svelte` and `.js` files by default.
|
||||
* Disable this if you'd like to use dynamic types.
|
||||
*/
|
||||
"checkJs": true
|
||||
},
|
||||
/**
|
||||
* Use global.d.ts instead of compilerOptions.types
|
||||
* to avoid limiting type declarations.
|
||||
*/
|
||||
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"]
|
||||
}
|
5565
package-lock.json
generated
Normal file
5565
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
package.json
Normal file
26
package.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "gpstool",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@garraflavatra/yeslint": "^1.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^2.4.2",
|
||||
"eslint": "^8.51.0",
|
||||
"eslint-plugin-svelte": "^2.34.0",
|
||||
"svelte": "^4.0.5",
|
||||
"vite": "^4.4.5"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "./node_modules/@garraflavatra/yeslint/configs/svelte.js",
|
||||
"rules": {
|
||||
"array-bracket-newline": "off",
|
||||
"array-element-newline": "off"
|
||||
}
|
||||
}
|
||||
}
|
79
src/app.css
Normal file
79
src/app.css
Normal file
@ -0,0 +1,79 @@
|
||||
html, body {
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
* {
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 32px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
ul li, ol li {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
dl {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
dl dt {
|
||||
font-weight: 700;
|
||||
}
|
||||
dl dd {
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: #fff;
|
||||
padding: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.card h2 {
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.input {
|
||||
min-width: 100px;
|
||||
padding: 2px 4px;
|
||||
font: inherit;
|
||||
border: 1px solid #888;
|
||||
border-radius: 4px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
*:last-child { margin-bottom: 0; }
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html, body {
|
||||
background-color: #333;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
a { color: #ade; }
|
||||
|
||||
.card { background-color: #444; }
|
||||
.card h2 { border-color: #333; }
|
||||
}
|
8
src/app.js
Normal file
8
src/app.js
Normal file
@ -0,0 +1,8 @@
|
||||
import './app.css';
|
||||
import App from './app.svelte';
|
||||
|
||||
const app = new App({
|
||||
target: document.getElementById('app'),
|
||||
});
|
||||
|
||||
export default app;
|
116
src/app.svelte
Normal file
116
src/app.svelte
Normal file
@ -0,0 +1,116 @@
|
||||
<script>
|
||||
/* global L */
|
||||
import { onMount } from 'svelte';
|
||||
import { latLngToDMS, latLngToDecimal, latLngToExif } from './conversion.js';
|
||||
import { getAppleUrl, getDuckDuckGoUrl, getGoogleUrl } from './urls.js';
|
||||
|
||||
let map;
|
||||
let mapDiv;
|
||||
let marker;
|
||||
let coords = L.latLng(51.507, -0.1275);
|
||||
|
||||
function updateMap() {
|
||||
map.setView(coords);
|
||||
marker.setLatLng(coords);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
map = L.map(mapDiv).setView(coords, 9);
|
||||
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
||||
}).addTo(map);
|
||||
|
||||
L.polyline([ [ -90, 0 ], [ 90, 0 ] ]).addTo(map);
|
||||
L.polyline([ [ 0, -180 ], [ 0, 180 ] ]).addTo(map);
|
||||
|
||||
marker = L.marker(coords).addTo(map);
|
||||
|
||||
map.on('click', event => {
|
||||
coords = event.latlng;
|
||||
updateMap();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<main class="grid">
|
||||
<div class="info">
|
||||
<section class="card">
|
||||
<h2>Coordinates</h2>
|
||||
|
||||
<dl>
|
||||
<dt>Decimal</dt>
|
||||
<dd>{latLngToDecimal(coords)}</dd>
|
||||
|
||||
<dt>DMS</dt>
|
||||
<dd>{latLngToDMS(coords)}</dd>
|
||||
|
||||
<dt>EXIF</dt>
|
||||
<dd>{latLngToExif(coords)}</dd>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Go to</h2>
|
||||
<div class="coordinate-input">
|
||||
<input
|
||||
class="input"
|
||||
type="number"
|
||||
step={0.25}
|
||||
bind:value={coords.lat}
|
||||
on:change={updateMap}
|
||||
/>
|
||||
<input
|
||||
class="input"
|
||||
type="number"
|
||||
step={0.25}
|
||||
bind:value={coords.lng}
|
||||
on:change={updateMap}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Open in</h2>
|
||||
<ul>
|
||||
<li><a href={getAppleUrl(coords)} target="_blank">Apple Maps</a></li>
|
||||
<li><a href={getGoogleUrl(coords)} target="_blank">Google Maps</a></li>
|
||||
<li><a href={getDuckDuckGoUrl(coords)} target="_blank">DuckDuckGo</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<p>
|
||||
Star gpstool on
|
||||
<a href="https://github.com/garraflavatra/gpstool" target="_blank" rel="noopener noreferrer">
|
||||
GitHub
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="map" bind:this={mapDiv} />
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template: 1fr / 250px 1fr;
|
||||
height: 100%;
|
||||
gap: 12px;
|
||||
}
|
||||
@media (max-width: 750px) {
|
||||
.grid {
|
||||
grid-template: 1fr 2fr / 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.coordinate-input {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
grid-template: 1fr / 1fr 1fr;
|
||||
}
|
||||
</style>
|
37
src/conversion.js
Normal file
37
src/conversion.js
Normal file
@ -0,0 +1,37 @@
|
||||
export function latLngToDecimal({ lat, lng }) {
|
||||
const precision = 5;
|
||||
return `${lat.toFixed(precision)}; ${lng.toFixed(precision)}`;
|
||||
}
|
||||
|
||||
// DMS = degrees, minutes, seconds
|
||||
function decimalToDMS(coord) {
|
||||
const negative = coord < 0;
|
||||
coord = Math.abs(coord);
|
||||
|
||||
const degrees = Math.trunc(coord);
|
||||
let minutes = (coord - degrees) * 60;
|
||||
const seconds = Math.trunc((minutes - Math.trunc(minutes)) * 60);
|
||||
minutes = Math.trunc(minutes);
|
||||
|
||||
return { degrees, minutes, seconds, negative };
|
||||
}
|
||||
|
||||
export function latLngToDMS({ lat, lng }) {
|
||||
lat = decimalToDMS(lat);
|
||||
lng = decimalToDMS(lng);
|
||||
const latRef = lat.negative ? 'S' : 'N';
|
||||
const lngRef = lng.negative ? 'W' : 'E';
|
||||
|
||||
return `${lat.degrees}°${lat.minutes}'${lat.seconds}"${latRef} ` +
|
||||
`${lng.degrees}°${lng.minutes}'${lng.seconds}"${lngRef}`;
|
||||
}
|
||||
|
||||
export function latLngToExif({ lat, lng }) {
|
||||
lat = decimalToDMS(lat);
|
||||
lng = decimalToDMS(lng);
|
||||
const latRef = lat.negative ? 'S' : 'N';
|
||||
const lngRef = lng.negative ? 'W' : 'E';
|
||||
|
||||
return `${lat.degrees},${lat.minutes}.${lat.seconds}${latRef} ` +
|
||||
`${lng.degrees},${lng.minutes}.${lng.seconds}${lngRef}`;
|
||||
}
|
17
src/urls.js
Normal file
17
src/urls.js
Normal file
@ -0,0 +1,17 @@
|
||||
import { latLngToDMS } from './conversion.js';
|
||||
|
||||
export function getAppleUrl({ lat, lng }) {
|
||||
return `https://maps.apple.com/?address=${lat},${lng}`;
|
||||
}
|
||||
|
||||
export function getGoogleUrl(latlng) {
|
||||
return `https://www.google.com/maps/place/${encodeURIComponent(
|
||||
latLngToDMS(latlng)
|
||||
)}`;
|
||||
}
|
||||
|
||||
export function getDuckDuckGoUrl(latlng) {
|
||||
return `https://duckduckgo.com/?q=${encodeURIComponent(
|
||||
latLngToDMS(latlng)
|
||||
)}&iaxm=maps`;
|
||||
}
|
2
src/vite-env.d.ts
vendored
Normal file
2
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/// <reference types="svelte" />
|
||||
/// <reference types="vite/client" />
|
8
svelte.config.js
Normal file
8
svelte.config.js
Normal file
@ -0,0 +1,8 @@
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
export default {
|
||||
preprocess: vitePreprocess(),
|
||||
compilerOptions: {
|
||||
cssHash: ({ css, hash }) => `_${hash(css)}`,
|
||||
},
|
||||
};
|
6
vite.config.js
Normal file
6
vite.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [ svelte() ],
|
||||
});
|
Loading…
Reference in New Issue
Block a user