Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(new tool): Geo Distance Computer #1456

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .eslintrc-auto-import.json
Original file line number Diff line number Diff line change
@@ -286,6 +286,9 @@
"watchTriggerable": true,
"watchWithFilter": true,
"whenever": true,
"toValue": true
"toValue": true,
"injectLocal": true,
"provideLocal": true,
"useClipboardItems": true
}
}
9 changes: 9 additions & 0 deletions auto-imports.d.ts
Original file line number Diff line number Diff line change
@@ -36,6 +36,7 @@ declare global {
const h: typeof import('vue')['h']
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
const inject: typeof import('vue')['inject']
const injectLocal: typeof import('@vueuse/core')['injectLocal']
const isDefined: typeof import('@vueuse/core')['isDefined']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
@@ -65,6 +66,7 @@ declare global {
const onUpdated: typeof import('vue')['onUpdated']
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
const provide: typeof import('vue')['provide']
const provideLocal: typeof import('@vueuse/core')['provideLocal']
const reactify: typeof import('@vueuse/core')['reactify']
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
const reactive: typeof import('vue')['reactive']
@@ -128,6 +130,7 @@ declare global {
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
const useCached: typeof import('@vueuse/core')['useCached']
const useClipboard: typeof import('@vueuse/core')['useClipboard']
const useClipboardItems: typeof import('@vueuse/core')['useClipboardItems']
const useCloned: typeof import('@vueuse/core')['useCloned']
const useColorMode: typeof import('@vueuse/core')['useColorMode']
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
@@ -326,6 +329,7 @@ declare module 'vue' {
readonly h: UnwrapRef<typeof import('vue')['h']>
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
readonly inject: UnwrapRef<typeof import('vue')['inject']>
readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
@@ -355,6 +359,7 @@ declare module 'vue' {
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
@@ -418,6 +423,7 @@ declare module 'vue' {
readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']>
readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']>
readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']>
readonly useClipboardItems: UnwrapRef<typeof import('@vueuse/core')['useClipboardItems']>
readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
@@ -610,6 +616,7 @@ declare module '@vue/runtime-core' {
readonly h: UnwrapRef<typeof import('vue')['h']>
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
readonly inject: UnwrapRef<typeof import('vue')['inject']>
readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
@@ -639,6 +646,7 @@ declare module '@vue/runtime-core' {
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
@@ -702,6 +710,7 @@ declare module '@vue/runtime-core' {
readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']>
readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']>
readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']>
readonly useClipboardItems: UnwrapRef<typeof import('@vueuse/core')['useClipboardItems']>
readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
5 changes: 3 additions & 2 deletions components.d.ts
Original file line number Diff line number Diff line change
@@ -80,6 +80,7 @@ declare module '@vue/runtime-core' {
EtaCalculator: typeof import('./src/tools/eta-calculator/eta-calculator.vue')['default']
FavoriteButton: typeof import('./src/components/FavoriteButton.vue')['default']
FormatTransformer: typeof import('./src/components/FormatTransformer.vue')['default']
GeoDistanceCalculator: typeof import('./src/tools/geo-distance-calculator/geo-distance-calculator.vue')['default']
GitMemo: typeof import('./src/tools/git-memo/git-memo.vue')['default']
'GitMemo.content': typeof import('./src/tools/git-memo/git-memo.content.md')['default']
HashText: typeof import('./src/tools/hash-text/hash-text.vue')['default']
@@ -130,19 +131,19 @@ declare module '@vue/runtime-core' {
MetaTagGenerator: typeof import('./src/tools/meta-tag-generator/meta-tag-generator.vue')['default']
MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default']
NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default']
NCheckbox: typeof import('naive-ui')['NCheckbox']
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
NDivider: typeof import('naive-ui')['NDivider']
NEllipsis: typeof import('naive-ui')['NEllipsis']
NFormItem: typeof import('naive-ui')['NFormItem']
NH1: typeof import('naive-ui')['NH1']
NH3: typeof import('naive-ui')['NH3']
NIcon: typeof import('naive-ui')['NIcon']
NInputNumber: typeof import('naive-ui')['NInputNumber']
NLayout: typeof import('naive-ui')['NLayout']
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
NMenu: typeof import('naive-ui')['NMenu']
NSpace: typeof import('naive-ui')['NSpace']
NTable: typeof import('naive-ui')['NTable']
NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default']
OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default']
PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.vue')['default']
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -46,6 +46,7 @@
"@tiptap/starter-kit": "2.1.6",
"@tiptap/vue-3": "2.0.3",
"@types/figlet": "^1.5.8",
"@types/haversine": "^1.1.8",
"@types/markdown-it": "^13.0.7",
"@vicons/material": "^0.12.0",
"@vicons/tabler": "^0.12.0",
@@ -67,6 +68,7 @@
"figlet": "^1.7.0",
"figue": "^1.2.0",
"fuse.js": "^6.6.2",
"haversine": "^1.1.1",
"highlight.js": "^11.7.0",
"iarna-toml-esm": "^3.0.5",
"ibantools": "^4.3.3",
14,628 changes: 6,511 additions & 8,117 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/modules/command-palette/command-palette.vue
Original file line number Diff line number Diff line change
@@ -128,7 +128,7 @@ function activateOption(option: PaletteOption) {
<c-input-text ref="inputRef" v-model:value="searchPrompt" raw-text placeholder="Type to search a tool or a command..." autofocus clearable />

<div v-for="(options, category) in filteredSearchResult" :key="category">
<div ml-3 mt-3 text-sm font-bold text-primary op-60>
<div ml-3 mt-3 text-sm text-primary font-bold op-60>
{{ category }}
</div>
<command-palette-option v-for="option in options" :key="option.name" :option="option" :selected="selectedOptionIndex === getOptionIndex(option)" @activated="activateOption" />
80 changes: 80 additions & 0 deletions src/tools/geo-distance-calculator/geo-distance-calculator.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<script setup lang="ts">
import haversine from 'haversine';
import SpanCopyable from '@/components/SpanCopyable.vue';
import { useQueryParamOrStorage } from '@/composable/queryParams';
const latitude1 = useQueryParamOrStorage({ name: 'lat1', storageName: 'geo-dist:lat1', defaultValue: 0 });
const longitude1 = useQueryParamOrStorage({ name: 'lng1', storageName: 'geo-dist:lng1', defaultValue: 0 });
const latitude2 = useQueryParamOrStorage({ name: 'lat2', storageName: 'geo-dist:lat2', defaultValue: 0 });
const longitude2 = useQueryParamOrStorage({ name: 'lng2', storageName: 'geo-dist:lng2', defaultValue: 0 });
const start = computed(() => ({ latitude: latitude1.value, longitude: longitude1.value }));
const end = computed(() => ({ latitude: latitude2.value, longitude: longitude2.value }));
const { coords: userCoords } = useGeolocation();
const distanceKM = computed(() => haversine(start.value, end.value, { unit: 'km' }));
const distanceMeter = computed(() => haversine(start.value, end.value, { unit: 'meter' }));
const distanceMile = computed(() => haversine(start.value, end.value, { unit: 'mile' }));
const distanceNMI = computed(() => haversine(start.value, end.value, { unit: 'nmi' }));
</script>

<template>
<div>
<c-card title="Distance computer" mb-2>
<n-form-item label="Geolocation 1 (latitude, longitude):">
<n-space justify="space-between">
<n-input-number v-model:value="latitude1" :min="-90" :max="90" placeholder="Latitude..." />
<n-input-number v-model:value="longitude1" :min="-180" :max="180" placeholder="Longitude..." />
</n-space>
</n-form-item>
<n-form-item label="Geolocation 2 (latitude, longitude):" gap-2>
<n-space justify="space-between">
<n-input-number v-model:value="latitude2" :min="-90" :max="90" placeholder="Latitude..." />
<n-input-number v-model:value="longitude2" :min="-180" :max="180" placeholder="Longitude..." />
</n-space>
</n-form-item>

<n-divider />

<n-form-item label="Distance (km):" label-width="120px" label-placement="left">
<SpanCopyable :value="distanceKM.toString()" />
</n-form-item>
<n-form-item label="Distance (mile):" label-width="120px" label-placement="left">
<SpanCopyable :value="distanceMile.toString()" />
</n-form-item>
<n-form-item label="Distance (meter):" label-width="120px" label-placement="left">
<SpanCopyable :value="distanceMeter.toString()" />
</n-form-item>
<n-form-item label="Distance (nmi):" label-width="120px" label-placement="left">
<SpanCopyable :value="distanceNMI.toString()" />
</n-form-item>
</c-card>

<c-card title="Your position">
<n-space justify="center">
<n-form-item label="Latitude:" label-placement="left">
<SpanCopyable :value="userCoords?.latitude?.toString() || 'Unknown'" />
</n-form-item>
<n-form-item label="Longitude:" label-placement="left">
<SpanCopyable :value="userCoords?.longitude?.toString() || 'Unknown'" />
</n-form-item>
</n-space>
<n-space justify="center">
<n-form-item label="Altitude:" label-placement="left">
<SpanCopyable :value="userCoords?.altitude?.toString() || 'Unknown'" />
</n-form-item>
<n-form-item label="Heading:" label-placement="left">
<SpanCopyable :value="userCoords?.heading?.toString() || 'Unknown'" />
</n-form-item>
<n-form-item label="Speed:" label-placement="left">
<SpanCopyable :value="userCoords?.speed?.toString() || 'Unknown'" />
</n-form-item>
<n-form-item label="Accuracy:" label-placement="left">
<SpanCopyable :value="userCoords?.accuracy?.toString() || 'Unknown'" />
</n-form-item>
</n-space>
</c-card>
</div>
</template>
12 changes: 12 additions & 0 deletions src/tools/geo-distance-calculator/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { WorldLatitude } from '@vicons/tabler';
import { defineTool } from '../tool';

export const tool = defineTool({
name: 'Geo distance calculator',
path: '/geo-distance-calculator',
description: 'Compute distance between two geo location (and display current user location information)',
keywords: ['geo', 'distance', 'calculator'],
component: () => import('./geo-distance-calculator.vue'),
icon: WorldLatitude,
createdAt: new Date('2025-01-01'),
});
8 changes: 7 additions & 1 deletion src/tools/index.ts
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@ import { tool as base64FileConverter } from './base64-file-converter';
import { tool as base64StringConverter } from './base64-string-converter';
import { tool as basicAuthGenerator } from './basic-auth-generator';
import { tool as emailNormalizer } from './email-normalizer';
import { tool as geoDistanceCalculator } from './geo-distance-calculator';

import { tool as asciiTextDrawer } from './ascii-text-drawer';

@@ -172,7 +173,12 @@ export const toolsByCategory: ToolCategory[] = [
},
{
name: 'Measurement',
components: [chronometer, temperatureConverter, benchmarkBuilder],
components: [
chronometer,
temperatureConverter,
benchmarkBuilder,
geoDistanceCalculator,
],
},
{
name: 'Text',
2 changes: 1 addition & 1 deletion src/ui/c-select/c-select.vue
Original file line number Diff line number Diff line change
@@ -151,7 +151,7 @@ function onSearchInput() {
>
<div flex-1 truncate>
<slot name="displayed-value">
<input v-if="searchable && isOpen" ref="searchInputRef" v-model="searchQuery" type="text" placeholder="Search..." class="search-input" w-full lh-normal color-current @input="onSearchInput">
<input v-if="searchable && isOpen" ref="searchInputRef" v-model="searchQuery" type="text" placeholder="Search..." class="search-input" w-full color-current lh-normal @input="onSearchInput">
<span v-else-if="selectedOption" lh-normal>
{{ selectedOption.label }}
</span>
2 changes: 1 addition & 1 deletion src/ui/c-table/c-table.vue
Original file line number Diff line number Diff line change
@@ -39,7 +39,7 @@ const headers = computed(() => {
<template>
<div class="relative overflow-x-auto rounded">
<table class="w-full border-collapse text-left text-sm text-gray-500 dark:text-gray-400" role="table" :aria-label="description">
<thead v-if="!hideHeaders" class="bg-#ffffff uppercase text-gray-700 dark:bg-#333333 dark:text-gray-400" border-b="1px solid dark:transparent #efeff5">
<thead v-if="!hideHeaders" class="bg-#ffffff text-gray-700 uppercase dark:bg-#333333 dark:text-gray-400" border-b="1px solid dark:transparent #efeff5">
<tr>
<th v-for="header in headers" :key="header.key" scope="col" class="px-6 py-3 text-xs">
{{ header.label }}