Add Map and Filter
This commit is contained in:
+43
-14
@@ -5,6 +5,7 @@ import { computed, ref } from 'vue';
|
||||
import { useThemeStore } from '@/stores/theme';
|
||||
import { useMenuStore } from '@/stores/menu';
|
||||
import { SUPPORT_LOCALES } from '@/i18n';
|
||||
import { useMapStore } from '@/stores/map';
|
||||
|
||||
const theme = useThemeStore();
|
||||
const router = useRouter();
|
||||
@@ -17,6 +18,7 @@ const activeRoute = computed({
|
||||
}
|
||||
});
|
||||
const menuStore = useMenuStore();
|
||||
const mapStore = useMapStore();
|
||||
const locales = ref(SUPPORT_LOCALES);
|
||||
</script>
|
||||
|
||||
@@ -37,35 +39,55 @@ const locales = ref(SUPPORT_LOCALES);
|
||||
:icon="menuStore.items[0].icon"
|
||||
:rightIcon="menuStore.items[0].rightIcon"
|
||||
@selected="() => menuStore.click(0)"
|
||||
>{{ menuStore.items[0].text }}</VaMenuItem
|
||||
>{{
|
||||
$te(menuStore.items[0].text)
|
||||
? $t(menuStore.items[0].text)
|
||||
: menuStore.items[0].text
|
||||
}}</VaMenuItem
|
||||
>
|
||||
<VaMenuItem
|
||||
v-if="menuStore.items.length > 1"
|
||||
:icon="menuStore.items[1].icon"
|
||||
:rightIcon="menuStore.items[1].rightIcon"
|
||||
@selected="() => menuStore.click(1)"
|
||||
>{{ menuStore.items[1].text }}</VaMenuItem
|
||||
>{{
|
||||
$te(menuStore.items[1].text)
|
||||
? $t(menuStore.items[1].text)
|
||||
: menuStore.items[1].text
|
||||
}}</VaMenuItem
|
||||
>
|
||||
<VaMenuItem
|
||||
v-if="menuStore.items.length > 2"
|
||||
:icon="menuStore.items[2].icon"
|
||||
:rightIcon="menuStore.items[2].rightIcon"
|
||||
@selected="() => menuStore.click(2)"
|
||||
>{{ menuStore.items[2].text }}</VaMenuItem
|
||||
>{{
|
||||
$te(menuStore.items[2].text)
|
||||
? $t(menuStore.items[2].text)
|
||||
: menuStore.items[2].text
|
||||
}}</VaMenuItem
|
||||
>
|
||||
<VaMenuItem
|
||||
v-if="menuStore.items.length > 3"
|
||||
:icon="menuStore.items[3].icon"
|
||||
:rightIcon="menuStore.items[3].rightIcon"
|
||||
@selected="() => menuStore.click(3)"
|
||||
>{{ menuStore.items[3].text }}</VaMenuItem
|
||||
>{{
|
||||
$te(menuStore.items[3].text)
|
||||
? $t(menuStore.items[3].text)
|
||||
: menuStore.items[3].text
|
||||
}}</VaMenuItem
|
||||
>
|
||||
<VaMenuItem
|
||||
v-if="menuStore.items.length > 4"
|
||||
:icon="menuStore.items[4].icon"
|
||||
:rightIcon="menuStore.items[4].rightIcon"
|
||||
@selected="() => menuStore.click(4)"
|
||||
>{{ menuStore.items[4].text }}</VaMenuItem
|
||||
>{{
|
||||
$te(menuStore.items[4].text)
|
||||
? $t(menuStore.items[4].text)
|
||||
: menuStore.items[4].text
|
||||
}}</VaMenuItem
|
||||
>
|
||||
<VaDivider />
|
||||
<VaMenuItem>
|
||||
@@ -73,24 +95,27 @@ const locales = ref(SUPPORT_LOCALES);
|
||||
v-model="theme.mode"
|
||||
true-value="dark"
|
||||
false-value="light"
|
||||
label="Toggle dark mode"
|
||||
:label="$t('toggle_dark_mode')"
|
||||
size="small"
|
||||
/></VaMenuItem>
|
||||
<VaMenuItem
|
||||
><VaSelect
|
||||
class="w-full"
|
||||
v-model="theme.lang"
|
||||
label="Language"
|
||||
:label="$t('i18n.language')"
|
||||
:options="locales"
|
||||
value-by="code"
|
||||
track-by="code"
|
||||
:text-by="(l) => $t(`locales.${(l as any)?.code || 'en'}`)"
|
||||
placeholder="Select an option"
|
||||
:text-by="(l) => $t(`i18n.locales.${(l as any)?.code || 'en'}`)"
|
||||
:placeholder="$t('i18n.select')"
|
||||
/></VaMenuItem>
|
||||
</VaMenu>
|
||||
</template>
|
||||
<template #left>
|
||||
<VaNavbarItem class="text-lg font-bold"> LOGO </VaNavbarItem>
|
||||
<VaNavbarItem class="text-lg font-bold">OSM Filter</VaNavbarItem>
|
||||
<VaNavbarItem v-if="mapStore.loading"
|
||||
><VaIcon name="sync" spin="counter-clockwise"
|
||||
/></VaNavbarItem>
|
||||
</template>
|
||||
</VaNavbar>
|
||||
</template>
|
||||
@@ -103,10 +128,14 @@ const locales = ref(SUPPORT_LOCALES);
|
||||
v-model="activeRoute"
|
||||
>
|
||||
<template #tabs>
|
||||
<VaTab name="/"><VaIcon name="house" />Home</VaTab>
|
||||
<VaTab name="/map"><VaIcon name="house" />Home</VaTab>
|
||||
<VaTab name="/filter"><VaIcon name="house" />filter</VaTab>
|
||||
<VaTab name="/about"><VaIcon name="house" />Home</VaTab>
|
||||
<VaTab name="/"><VaIcon name="house" />{{ $t('tabs.home') }}</VaTab>
|
||||
<VaTab name="/map"><VaIcon name="map" />{{ $t('tabs.map') }}</VaTab>
|
||||
<VaTab name="/filter"
|
||||
><VaIcon name="filter_alt" />{{ $t('tabs.filter') }}</VaTab
|
||||
>
|
||||
<VaTab name="/about"
|
||||
><VaIcon name="info" />{{ $t('tabs.about') }}</VaTab
|
||||
>
|
||||
</template>
|
||||
</VaTabs>
|
||||
</template>
|
||||
|
||||
@@ -9,3 +9,7 @@ body {
|
||||
background-color: var(--va-background-primary);
|
||||
color: var(--va-text-primary);
|
||||
}
|
||||
|
||||
.text-content {
|
||||
@apply sm:mx-auto sm:w-2/3 md:w-1/2;
|
||||
}
|
||||
|
||||
+45342
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,77 @@
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, resolve } from 'path';
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
|
||||
type OSMAmenity = {
|
||||
value: string;
|
||||
in_wiki: boolean;
|
||||
lang: Record<string, string>;
|
||||
};
|
||||
const url = (lang: string, page: number, perPage = 999) =>
|
||||
`https://taginfo.openstreetmap.org/api/4/key/values?key=amenity&filter=all&lang=${lang}&sortname=count&sortorder=desc&rp=${perPage}&page=${page}`;
|
||||
|
||||
async function getPage(lang: string, page: number, perPage = 999) {
|
||||
return fetch(url(lang, page, perPage))
|
||||
.then<TagInfoApi<KeyValues>>((b) => b.json() as any)
|
||||
.then((r) => r.data);
|
||||
}
|
||||
export interface TagInfoApi<E> {
|
||||
url: string;
|
||||
page: number;
|
||||
rp: number;
|
||||
total: number;
|
||||
data: E[];
|
||||
}
|
||||
|
||||
export interface KeyValues {
|
||||
value: string;
|
||||
count: number;
|
||||
fraction: number;
|
||||
in_wiki: boolean;
|
||||
description: string;
|
||||
desclang: string;
|
||||
descdir: 'ltr' | 'rtl';
|
||||
}
|
||||
|
||||
const { total } = await fetch(url('en', 1, 1)).then<TagInfoApi<KeyValues>>(
|
||||
(b) => b.json() as any
|
||||
);
|
||||
|
||||
console.log(total);
|
||||
const all: OSMAmenity[] = await Promise.all(
|
||||
new Array(Math.ceil(total / 999))
|
||||
.fill(null)
|
||||
.map((_, i) => getPage('en', i + 1))
|
||||
).then((r) =>
|
||||
r.flatMap((k) =>
|
||||
k.map((kv) => ({
|
||||
value: kv.value,
|
||||
in_wiki: kv.in_wiki,
|
||||
lang: { [kv.desclang]: kv.description }
|
||||
}))
|
||||
)
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
new Array(Math.ceil(total / 999))
|
||||
.fill(null)
|
||||
.map((_, i) => getPage('de', i + 1))
|
||||
)
|
||||
.then((r) => r.flat())
|
||||
.then((r) =>
|
||||
r.forEach(
|
||||
(a) =>
|
||||
(all.find((al) => al.value === a.value)!!.lang[a.desclang] =
|
||||
a.description)
|
||||
)
|
||||
);
|
||||
|
||||
console.log(all);
|
||||
const modulePath = dirname(fileURLToPath(import.meta.url));
|
||||
await writeFile(
|
||||
resolve(modulePath, '../data/amenities.json'),
|
||||
JSON.stringify(all, undefined, 2),
|
||||
{
|
||||
flag: 'w+'
|
||||
}
|
||||
);
|
||||
+27
-3
@@ -1,7 +1,31 @@
|
||||
{
|
||||
"text": "Hallo",
|
||||
"locales": {
|
||||
"en": "Englisch",
|
||||
"de": "Deutsch"
|
||||
"map": {
|
||||
"attribution": "Kartendaten © {a} Mitwirkende"
|
||||
},
|
||||
"tabs": {
|
||||
"home": "Start",
|
||||
"map": "Karte",
|
||||
"filter": "Filter",
|
||||
"about": "Über"
|
||||
},
|
||||
"pages": {
|
||||
"map": {},
|
||||
"filter": {
|
||||
"title": "Filter"
|
||||
},
|
||||
"about": {
|
||||
"title": "Über uns"
|
||||
}
|
||||
},
|
||||
"toggle_dark_mode": "Darkmode umschalten",
|
||||
|
||||
"i18n": {
|
||||
"language": "Sprache",
|
||||
"select": "Wähle eine Sprache aus",
|
||||
"locales": {
|
||||
"en": "Englisch",
|
||||
"de": "Deutsch"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+26
-3
@@ -1,7 +1,30 @@
|
||||
{
|
||||
"text": "Hello",
|
||||
"locales": {
|
||||
"en": "English",
|
||||
"de": "German"
|
||||
"map": {
|
||||
"attribution": "Map data © {a} contributors"
|
||||
},
|
||||
"tabs": {
|
||||
"home": "Home",
|
||||
"map": "Map",
|
||||
"filter": "Filter",
|
||||
"about": "About"
|
||||
},
|
||||
"pages": {
|
||||
"map": {},
|
||||
"filter": {
|
||||
"title": "Filter"
|
||||
},
|
||||
"about": {
|
||||
"title": "About us"
|
||||
}
|
||||
},
|
||||
"toggle_dark_mode": "Toggle dark mode",
|
||||
"i18n": {
|
||||
"language": "Language",
|
||||
"select": "Select a language",
|
||||
"locales": {
|
||||
"en": "English",
|
||||
"de": "German"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
import _amenities from './data/amenities.json';
|
||||
|
||||
export type OSMAmenity = {
|
||||
value: string;
|
||||
in_wiki: boolean;
|
||||
lang: {
|
||||
en?: string;
|
||||
de?: string;
|
||||
};
|
||||
};
|
||||
export const amenities: OSMAmenity[] = _amenities;
|
||||
|
||||
export interface APIElement {
|
||||
id: string;
|
||||
tags: Record<string, string>;
|
||||
}
|
||||
|
||||
//
|
||||
// Overpass
|
||||
//
|
||||
|
||||
export interface NodeElement extends APIElement {
|
||||
type: 'node';
|
||||
lat: number;
|
||||
lon: number;
|
||||
}
|
||||
|
||||
export interface WayElement extends APIElement {
|
||||
type: 'way';
|
||||
bounds: {
|
||||
minlat: number;
|
||||
minlon: number;
|
||||
maxlat: number;
|
||||
maxlon: number;
|
||||
};
|
||||
nodes: number[];
|
||||
geometry: { lat: number; lon: number }[];
|
||||
}
|
||||
|
||||
export interface RelationElement extends APIElement {
|
||||
type: 'relation';
|
||||
bounds: {
|
||||
minlat: number;
|
||||
minlon: number;
|
||||
maxlat: number;
|
||||
maxlon: number;
|
||||
};
|
||||
members: (Omit<
|
||||
NodeElement | WayElement | Omit<RelationElement, 'members'>,
|
||||
'id' | 'tags' | 'bounds'
|
||||
> & { role: string })[];
|
||||
}
|
||||
|
||||
export type NWRElement = Readonly<NodeElement | WayElement | RelationElement>;
|
||||
|
||||
export function isWay(e: { type: NWRElement['type'] }): e is WayElement {
|
||||
return e.type === 'way';
|
||||
}
|
||||
export function isNode(e: { type: NWRElement['type'] }): e is NodeElement {
|
||||
return e.type === 'node';
|
||||
}
|
||||
export function isRelation(e: {
|
||||
type: NWRElement['type'];
|
||||
}): e is RelationElement {
|
||||
return e.type === 'relation';
|
||||
}
|
||||
|
||||
export function tagsToHTML(tags: APIElement['tags']) {
|
||||
const entries = Object.entries(tags);
|
||||
|
||||
const keyValues = entries.map(
|
||||
([k, v]) => `<span class="font-bold">${k}:</span><span class="">${v}</span>`
|
||||
);
|
||||
return `<div class="grid grid-cols-[max-content_1fr] gap-2">${keyValues.join('')}</div>`;
|
||||
}
|
||||
+10
-3
@@ -12,10 +12,17 @@ const router = createRouter({
|
||||
{
|
||||
path: '/about',
|
||||
name: 'about',
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (About.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import('../views/AboutView.vue')
|
||||
},
|
||||
{
|
||||
path: '/map',
|
||||
name: 'map',
|
||||
component: () => import('../views/MapView.vue')
|
||||
},
|
||||
{
|
||||
path: '/filter',
|
||||
name: 'filter',
|
||||
component: () => import('../views/FilterView.vue')
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { acceptHMRUpdate, defineStore } from 'pinia';
|
||||
import { computed, reactive, readonly, ref, watch } from 'vue';
|
||||
import type { LatLngBounds } from 'leaflet';
|
||||
import {
|
||||
isNode,
|
||||
isRelation,
|
||||
isWay,
|
||||
type NodeElement,
|
||||
type NWRElement,
|
||||
type RelationElement,
|
||||
type WayElement
|
||||
} from '@/osm';
|
||||
|
||||
export const useMapStore = defineStore(
|
||||
'map',
|
||||
() => {
|
||||
// defaults to Germany
|
||||
const center = ref<[number, number]>([52.07838, 10.250244]);
|
||||
const zoom = ref(6);
|
||||
const bounds = ref<LatLngBounds>();
|
||||
|
||||
const filterForm = reactive({
|
||||
amenitySelect: [] as string[]
|
||||
});
|
||||
|
||||
const opQuery = computed(() => {
|
||||
const b = bounds.value;
|
||||
if (!b) {
|
||||
return null;
|
||||
}
|
||||
const nodes = filterForm.amenitySelect
|
||||
.map((a) => `nwr["amenity"="${a}"];`)
|
||||
.join('\n');
|
||||
return `[out:json][timeout:25][bbox:${b.getSouth()},${b.getWest()},${b.getNorth()},${b.getEast()}];
|
||||
(
|
||||
${nodes}
|
||||
);
|
||||
out geom;`;
|
||||
});
|
||||
|
||||
const opLoading = ref(false);
|
||||
|
||||
const nodes = ref<NodeElement[]>([]);
|
||||
const relations = ref<RelationElement[]>([]);
|
||||
const ways = ref<WayElement[]>([]);
|
||||
watch(
|
||||
opQuery,
|
||||
async (q) => {
|
||||
if (!q || opLoading.value || zoom.value < 12) {
|
||||
return;
|
||||
}
|
||||
opLoading.value = true;
|
||||
try {
|
||||
const elements = await fetch(
|
||||
'https://overpass-api.de/api/interpreter',
|
||||
{
|
||||
headers: {
|
||||
'content-type':
|
||||
'application/x-www-form-urlencoded; charset=UTF-8'
|
||||
},
|
||||
body: 'data=' + encodeURIComponent(q),
|
||||
method: 'POST'
|
||||
}
|
||||
)
|
||||
.then((b) => b.json())
|
||||
.catch(() => ({ elements: [] }))
|
||||
.then((r) => r.elements as NWRElement[]);
|
||||
|
||||
nodes.value = elements.filter(isNode);
|
||||
relations.value = elements.filter(isRelation);
|
||||
ways.value = elements.filter(isWay);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
opLoading.value = false;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
return {
|
||||
center,
|
||||
zoom,
|
||||
bounds,
|
||||
filterForm,
|
||||
opQuery,
|
||||
loading: readonly(opLoading),
|
||||
nodes,
|
||||
relations,
|
||||
ways
|
||||
};
|
||||
},
|
||||
{
|
||||
persist: {
|
||||
paths: ['center', 'zoom', 'filterForm']
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept(acceptHMRUpdate(useMapStore, import.meta.hot));
|
||||
}
|
||||
+2
-5
@@ -11,25 +11,22 @@ export const useThemeStore = defineStore(
|
||||
const h = document.querySelector('html');
|
||||
watch(mode, (newMode, oldMode) => {
|
||||
applyPreset(newMode);
|
||||
console.log(newMode);
|
||||
if (h?.classList.replace(oldMode, newMode) === false) {
|
||||
h?.classList.add(newMode);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(navigator.language);
|
||||
const lang = ref(
|
||||
SUPPORT_LOCALES.find((l) =>
|
||||
navigator.language.toLowerCase().startsWith(l.code)
|
||||
)?.code || SUPPORT_LOCALES[0].code
|
||||
);
|
||||
watch(lang, (l) => {
|
||||
console.log(l);
|
||||
i18n.global.locale.value = l as any;
|
||||
document.querySelector('html')?.setAttribute('lang', l);
|
||||
});
|
||||
|
||||
return { mode, lang };
|
||||
const fullName = ref(true);
|
||||
return { mode, lang, fullName };
|
||||
},
|
||||
{
|
||||
persist: {
|
||||
|
||||
+4
-12
@@ -1,15 +1,7 @@
|
||||
<template>
|
||||
<div class="about">
|
||||
<h1>This is an about page</h1>
|
||||
</div>
|
||||
<main class="text-content">
|
||||
<h1 class="mt-2 text-center text-3xl">{{ $t('pages.about.title') }}</h1>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@media (min-width: 1024px) {
|
||||
.about {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style></style>
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
<script setup lang="ts">
|
||||
import { useForm } from 'vuestic-ui';
|
||||
import { amenities } from '@/osm';
|
||||
import { useMapStore } from '@/stores/map';
|
||||
import { useThemeStore } from '@/stores/theme';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { isValid } = useForm('formRef');
|
||||
|
||||
const theme = useThemeStore();
|
||||
const mapStore = useMapStore();
|
||||
|
||||
const i18n = useI18n();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="text-content">
|
||||
<h1 class="mt-2 text-center text-3xl">
|
||||
<VaIcon name="filter_alt" size="large" />{{ $t('pages.filter.title') }}
|
||||
</h1>
|
||||
<div class="flex">
|
||||
<VaSwitch v-model="theme.fullName" label="Show full names" />
|
||||
</div>
|
||||
<VaForm ref="formRef">
|
||||
<VaAccordion>
|
||||
<VaCollapse>
|
||||
<template #header-content
|
||||
>Amenity
|
||||
<VaChip
|
||||
v-if="mapStore.filterForm.amenitySelect.length > 0"
|
||||
class="ml-auto"
|
||||
>{{ mapStore.filterForm.amenitySelect.length }}</VaChip
|
||||
></template
|
||||
>
|
||||
<template #content>
|
||||
<VaButton
|
||||
icon="delete"
|
||||
class="mb-3"
|
||||
color="danger"
|
||||
:disabled="mapStore.filterForm.amenitySelect.length <= 0"
|
||||
@click="() => mapStore.filterForm.amenitySelect.splice(0)"
|
||||
>Reset</VaButton
|
||||
>
|
||||
<VaVirtualScroller
|
||||
v-slot="{ item }"
|
||||
:items="amenities"
|
||||
wrapper-size="30rem"
|
||||
>
|
||||
<VaCheckbox
|
||||
preset="solid"
|
||||
class="my-3 ml-1"
|
||||
v-model="mapStore.filterForm.amenitySelect"
|
||||
:array-value="item.value"
|
||||
:label="
|
||||
!theme.fullName
|
||||
? item.value
|
||||
: i18n.locale.value in item.lang
|
||||
? item.lang[i18n.locale.value as 'en']
|
||||
: item.lang.en
|
||||
? item.lang.en
|
||||
: item.value
|
||||
"
|
||||
/>
|
||||
</VaVirtualScroller>
|
||||
</template>
|
||||
</VaCollapse>
|
||||
<VaCollapse>
|
||||
<template #header-content
|
||||
>Objects <VaChip class="ml-auto">1</VaChip></template
|
||||
>
|
||||
<template #content> </template>
|
||||
</VaCollapse>
|
||||
<VaCollapse>
|
||||
<template #header-content
|
||||
>Objects <VaChip class="ml-auto">1</VaChip></template
|
||||
>
|
||||
<template #content> </template>
|
||||
</VaCollapse>
|
||||
</VaAccordion>
|
||||
</VaForm>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -3,7 +3,7 @@ import TheWelcome from '../components/TheWelcome.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<main class="text-content">
|
||||
<TheWelcome />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<script setup lang="ts">
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import {
|
||||
LMap,
|
||||
LMarker,
|
||||
LPolygon,
|
||||
LPolyline,
|
||||
LPopup,
|
||||
LTileLayer
|
||||
} from '@vue-leaflet/vue-leaflet';
|
||||
import L, { LatLng } from 'leaflet';
|
||||
import markerIcon2x from 'leaflet/dist/images/marker-icon-2x.png';
|
||||
import markerIcon from 'leaflet/dist/images/marker-icon.png';
|
||||
import markerShadow from 'leaflet/dist/images/marker-shadow.png';
|
||||
import { useMapStore } from '@/stores/map';
|
||||
import { tagsToHTML } from '@/osm';
|
||||
|
||||
// @ts-ignore
|
||||
delete L.Icon.Default.prototype._getIconUrl;
|
||||
|
||||
L.Icon.Default.mergeOptions({
|
||||
iconRetinaUrl: markerIcon2x,
|
||||
iconUrl: markerIcon,
|
||||
shadowUrl: markerShadow
|
||||
});
|
||||
|
||||
const a =
|
||||
'<a href="https://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>';
|
||||
const mapStore = useMapStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="w-full" style="height: calc(100dvh - (48px + 75px))">
|
||||
<l-map
|
||||
ref="map"
|
||||
v-model:zoom="mapStore.zoom"
|
||||
:center="mapStore.center"
|
||||
@update:center="(nc: LatLng) => (mapStore.center = [nc.lat, nc.lng])"
|
||||
@update:bounds="(nb) => (mapStore.bounds = nb)"
|
||||
@ready="(lmap) => (mapStore.bounds = lmap.getBounds())"
|
||||
>
|
||||
<l-tile-layer
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
layer-type="base"
|
||||
name="OpenStreetMap"
|
||||
:attribution="$t('map.attribution', { a })"
|
||||
/>
|
||||
<l-marker
|
||||
v-for="node in mapStore.nodes"
|
||||
:key="node.id"
|
||||
:lat-lng="[node.lat, node.lon]"
|
||||
><l-popup
|
||||
><div class="flex flex-col">
|
||||
<div v-html="tagsToHTML(node.tags)" /></div></l-popup
|
||||
></l-marker>
|
||||
<l-polygon
|
||||
v-for="way in mapStore.ways"
|
||||
:key="`pg-${way.id}`"
|
||||
:lat-lngs="way.geometry.map((g) => [g.lat, g.lon])"
|
||||
><l-popup><div v-html="tagsToHTML(way.tags)" /></l-popup
|
||||
></l-polygon>
|
||||
<l-polyline
|
||||
v-for="way in mapStore.ways"
|
||||
:key="`pl-${way.id}`"
|
||||
:lat-lngs="way.geometry.map((g) => [g.lat, g.lon])"
|
||||
><l-popup><div v-html="tagsToHTML(way.tags)" /></l-popup
|
||||
></l-polyline>
|
||||
</l-map>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
Reference in New Issue
Block a user