mirror of
https://git.quad4.io/RNS-Things/MeshChatX.git
synced 2026-04-27 02:05:42 +00:00
feat(map): update map features with bearing mode, improved export options, and new vector exchange panel
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,46 @@
|
||||
<!-- SPDX-License-Identifier: 0BSD -->
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="absolute top-[calc(0.5rem+2.75rem+0.5rem+2.75rem)] left-1/2 -translate-x-1/2 z-[19] w-[min(100vw-2rem,24rem)] pointer-events-auto"
|
||||
>
|
||||
<div
|
||||
class="bg-white/95 dark:bg-zinc-900/95 backdrop-blur border border-gray-200 dark:border-zinc-700 rounded-xl shadow-lg px-3 py-2 text-xs text-gray-800 dark:text-zinc-200"
|
||||
>
|
||||
<p class="font-medium text-center" :class="showFromHere ? 'mb-2' : ''">{{ instructionText }}</p>
|
||||
<button
|
||||
v-if="showFromHere"
|
||||
type="button"
|
||||
class="w-full py-1.5 rounded-lg bg-emerald-600 hover:bg-emerald-500 text-white text-[11px] font-semibold transition-colors"
|
||||
@click="$emit('use-my-location')"
|
||||
>
|
||||
{{ $t("map.bearing_from_here") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "MapBearingInstructions",
|
||||
props: {
|
||||
fromGpsActive: { type: Boolean, default: false },
|
||||
awaitingSecondTap: { type: Boolean, default: false },
|
||||
},
|
||||
emits: ["use-my-location"],
|
||||
computed: {
|
||||
instructionText() {
|
||||
if (this.fromGpsActive) {
|
||||
return this.$t("map.bearing_hint_destination");
|
||||
}
|
||||
if (this.awaitingSecondTap) {
|
||||
return this.$t("map.bearing_hint_second");
|
||||
}
|
||||
return this.$t("map.bearing_hint_first");
|
||||
},
|
||||
showFromHere() {
|
||||
return !this.fromGpsActive;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -13,7 +13,7 @@
|
||||
:ref="tool.type === 'Export' ? 'exportToolButton' : null"
|
||||
class="p-1.5 sm:p-2 rounded-xl transition-all hover:scale-110 active:scale-90"
|
||||
:class="[
|
||||
(drawType === tool.type && !measuring) || (tool.type === 'Export' && exportMode)
|
||||
(drawType === tool.type && !measuring && !bearingMode) || (tool.type === 'Export' && exportMode)
|
||||
? 'bg-blue-500 text-white shadow-lg shadow-blue-500/30'
|
||||
: 'hover:bg-gray-100 dark:hover:bg-zinc-800 text-gray-600 dark:text-gray-300',
|
||||
]"
|
||||
@@ -26,7 +26,7 @@
|
||||
<button
|
||||
class="p-1.5 sm:p-2 rounded-xl transition-all hover:scale-110 active:scale-90"
|
||||
:class="[
|
||||
measuring
|
||||
measuring && !bearingMode
|
||||
? 'bg-indigo-500 text-white shadow-lg shadow-indigo-500/30'
|
||||
: 'hover:bg-gray-100 dark:hover:bg-zinc-800 text-gray-600 dark:text-gray-300',
|
||||
]"
|
||||
@@ -35,6 +35,30 @@
|
||||
>
|
||||
<v-icon icon="mdi-ruler" size="18" class="sm:!size-5"></v-icon>
|
||||
</button>
|
||||
<button
|
||||
class="p-1.5 sm:p-2 rounded-xl transition-all hover:scale-110 active:scale-90"
|
||||
:class="[
|
||||
bearingMode
|
||||
? 'bg-teal-600 text-white shadow-lg shadow-teal-600/30'
|
||||
: 'hover:bg-gray-100 dark:hover:bg-zinc-800 text-gray-600 dark:text-gray-300',
|
||||
]"
|
||||
:title="$t('map.tool_bearing')"
|
||||
@click="$emit('toggle-bearing')"
|
||||
>
|
||||
<v-icon icon="mdi-compass-outline" size="18" class="sm:!size-5"></v-icon>
|
||||
</button>
|
||||
<button
|
||||
class="p-1.5 sm:p-2 rounded-xl transition-all hover:scale-110 active:scale-90"
|
||||
:class="[
|
||||
bearingMode && bearingFromGps
|
||||
? 'bg-teal-600 text-white shadow-lg shadow-teal-600/30'
|
||||
: 'hover:bg-gray-100 dark:hover:bg-zinc-800 text-gray-600 dark:text-gray-300',
|
||||
]"
|
||||
:title="$t('map.tool_bearing_from_here')"
|
||||
@click="$emit('bearing-from-here')"
|
||||
>
|
||||
<v-icon icon="mdi-navigation-variant" size="18" class="sm:!size-5"></v-icon>
|
||||
</button>
|
||||
<button
|
||||
class="p-1.5 sm:p-2 rounded-xl hover:bg-red-50 dark:hover:bg-red-900/20 text-red-500 transition-all hover:scale-110 active:scale-90"
|
||||
:title="$t('map.tool_clear')"
|
||||
@@ -81,6 +105,20 @@
|
||||
>
|
||||
<v-icon icon="mdi-crosshairs-gps" size="18" class="sm:!size-5"></v-icon>
|
||||
</button>
|
||||
<button
|
||||
class="p-1.5 sm:p-2 rounded-xl hover:bg-emerald-50 dark:hover:bg-emerald-900/20 text-emerald-600 dark:text-emerald-400 transition-all hover:scale-110 active:scale-90"
|
||||
:title="$t('map.share_view')"
|
||||
@click="$emit('share-view')"
|
||||
>
|
||||
<v-icon icon="mdi-share-variant" size="18" class="sm:!size-5"></v-icon>
|
||||
</button>
|
||||
<button
|
||||
class="p-1.5 sm:p-2 rounded-xl hover:bg-amber-50 dark:hover:bg-amber-900/20 text-amber-600 dark:text-amber-400 transition-all hover:scale-110 active:scale-90"
|
||||
:title="$t('map.ping_here_toolbar')"
|
||||
@click="$emit('ping-here')"
|
||||
>
|
||||
<v-icon icon="mdi-send" size="18" class="sm:!size-5"></v-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -92,6 +130,8 @@ export default {
|
||||
tools: { type: Array, required: true },
|
||||
drawType: { type: String, default: null },
|
||||
measuring: { type: Boolean, default: false },
|
||||
bearingMode: { type: Boolean, default: false },
|
||||
bearingFromGps: { type: Boolean, default: false },
|
||||
exportMode: { type: Boolean, default: false },
|
||||
selectedFeature: { type: Object, default: null },
|
||||
},
|
||||
@@ -99,12 +139,16 @@ export default {
|
||||
"toggle-draw",
|
||||
"toggle-export",
|
||||
"toggle-measure",
|
||||
"toggle-bearing",
|
||||
"bearing-from-here",
|
||||
"clear",
|
||||
"edit-note",
|
||||
"delete-feature",
|
||||
"save",
|
||||
"load",
|
||||
"locate",
|
||||
"share-view",
|
||||
"ping-here",
|
||||
],
|
||||
methods: {
|
||||
onToolClick(tool) {
|
||||
|
||||
@@ -39,6 +39,9 @@
|
||||
<span class="text-gray-600 dark:text-zinc-400">{{ $t("map.tile_count") }}:</span>
|
||||
<span class="font-bold text-blue-600">{{ estimatedTiles }}</span>
|
||||
</div>
|
||||
<p v-if="tileLimitExceeded" class="text-xs text-red-600 dark:text-red-400 font-semibold">
|
||||
{{ $t("map.export_tile_limit_exceeded") }}
|
||||
</p>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
:disabled="exporting"
|
||||
@@ -48,7 +51,7 @@
|
||||
{{ $t("common.cancel") }}
|
||||
</button>
|
||||
<button
|
||||
:disabled="exporting"
|
||||
:disabled="exporting || tileLimitExceeded"
|
||||
class="flex-1 py-2 bg-blue-500 hover:bg-blue-600 disabled:bg-blue-300 text-white rounded-lg font-bold transition-colors shadow-md"
|
||||
@click="$emit('start')"
|
||||
>
|
||||
@@ -70,6 +73,7 @@ export default {
|
||||
maxZoom: { type: Number, required: true },
|
||||
estimatedTiles: { type: [Number, String], default: 0 },
|
||||
exporting: { type: Boolean, default: false },
|
||||
tileLimitExceeded: { type: Boolean, default: false },
|
||||
},
|
||||
emits: ["cancel", "start", "update:minZoom", "update:maxZoom"],
|
||||
};
|
||||
|
||||
@@ -2,14 +2,33 @@
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="absolute top-4 left-1/2 -translate-x-1/2 z-20 px-4 py-2 bg-blue-600 text-white rounded-full shadow-lg font-medium text-sm animate-bounce"
|
||||
class="absolute top-4 left-1/2 -translate-x-1/2 z-20 flex flex-col items-center gap-2 max-w-[min(100vw-2rem,36rem)] px-2"
|
||||
>
|
||||
{{ $t("map.export_instructions") }}
|
||||
<div
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded-full shadow-lg font-medium text-sm animate-bounce text-center"
|
||||
>
|
||||
{{ $t("map.export_instructions") }}
|
||||
</div>
|
||||
<div v-if="presets && presets.length" class="flex flex-wrap justify-center gap-2 pointer-events-auto">
|
||||
<button
|
||||
v-for="p in presets"
|
||||
:key="p.id"
|
||||
type="button"
|
||||
class="px-2 py-1 text-[10px] font-bold uppercase tracking-tight rounded-lg bg-white/95 dark:bg-zinc-900/95 border border-gray-200 dark:border-zinc-700 text-gray-800 dark:text-zinc-100 shadow-sm hover:bg-gray-50 dark:hover:bg-zinc-800"
|
||||
@click="$emit('select-preset', p)"
|
||||
>
|
||||
{{ $t(`map.export_region_${p.id}`) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "MapExportInstructions",
|
||||
props: {
|
||||
presets: { type: Array, default: () => [] },
|
||||
},
|
||||
emits: ["select-preset"],
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
<!-- SPDX-License-Identifier: 0BSD -->
|
||||
|
||||
<template>
|
||||
<div class="space-y-3 rounded-xl border border-gray-200 dark:border-zinc-800 bg-gray-50/50 dark:bg-zinc-900/40 p-3">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-[10px] font-bold text-gray-500 dark:text-zinc-500 uppercase tracking-widest">{{
|
||||
$t("map.vector_exchange_title")
|
||||
}}</span>
|
||||
</div>
|
||||
<label class="flex items-center gap-2 text-[10px] text-gray-600 dark:text-zinc-400 cursor-pointer select-none">
|
||||
<input v-model="mergeImport" type="checkbox" class="rounded border-gray-300 dark:border-zinc-600" />
|
||||
{{ $t("map.vector_exchange_merge") }}
|
||||
</label>
|
||||
<div class="grid grid-cols-2 gap-2 sm:grid-cols-3">
|
||||
<button
|
||||
type="button"
|
||||
class="py-2 px-2 text-[10px] font-bold uppercase rounded-lg bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-700 text-gray-800 dark:text-zinc-100 hover:bg-gray-50 dark:hover:bg-zinc-800 disabled:opacity-40"
|
||||
:disabled="disabled"
|
||||
@click="triggerGeoJsonPick"
|
||||
>
|
||||
{{ $t("map.vector_import_geojson") }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="py-2 px-2 text-[10px] font-bold uppercase rounded-lg bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-700 text-gray-800 dark:text-zinc-100 hover:bg-gray-50 dark:hover:bg-zinc-800 disabled:opacity-40"
|
||||
:disabled="disabled"
|
||||
@click="triggerKmlPick"
|
||||
>
|
||||
{{ $t("map.vector_import_kml") }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="py-2 px-2 text-[10px] font-bold uppercase rounded-lg bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-700 text-gray-800 dark:text-zinc-100 hover:bg-gray-50 dark:hover:bg-zinc-800 disabled:opacity-40"
|
||||
:disabled="disabled"
|
||||
@click="triggerKmzPick"
|
||||
>
|
||||
{{ $t("map.vector_import_kmz") }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center px-2 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-all text-[10px] font-bold uppercase tracking-tight shadow-sm active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:disabled="disabled || !hasFeatures"
|
||||
@click="$emit('export-geojson')"
|
||||
>
|
||||
{{ $t("map.vector_export_geojson") }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center px-2 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-all text-[10px] font-bold uppercase tracking-tight shadow-sm active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:disabled="disabled || !hasFeatures"
|
||||
@click="$emit('export-kml')"
|
||||
>
|
||||
{{ $t("map.vector_export_kml") }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center px-2 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-all text-[10px] font-bold uppercase tracking-tight shadow-sm active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:disabled="disabled || !hasFeatures"
|
||||
@click="$emit('export-kmz')"
|
||||
>
|
||||
{{ $t("map.vector_export_kmz") }}
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-[9px] text-gray-500 dark:text-zinc-500 leading-snug">
|
||||
{{ $t("map.vector_exchange_hint") }}
|
||||
</p>
|
||||
<input
|
||||
ref="geojsonInput"
|
||||
type="file"
|
||||
accept=".geojson,.json,application/geo+json,application/json"
|
||||
class="hidden"
|
||||
@change="onGeojsonFile"
|
||||
/>
|
||||
<input
|
||||
ref="kmlInput"
|
||||
type="file"
|
||||
accept=".kml,.xml,text/xml,application/vnd.google-earth.kml+xml"
|
||||
class="hidden"
|
||||
@change="onKmlFile"
|
||||
/>
|
||||
<input
|
||||
ref="kmzInput"
|
||||
type="file"
|
||||
accept=".kmz,application/vnd.google-earth.kmz,application/zip"
|
||||
class="hidden"
|
||||
@change="onKmzFile"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { readGeoJsonToFeatures } from "../../../js/mapExchange/geoJsonCodec.js";
|
||||
import { readKmlToFeatures } from "../../../js/mapExchange/kmlCodec.js";
|
||||
import { readKmzToFeatures } from "../../../js/mapExchange/kmzCodec.js";
|
||||
|
||||
export default {
|
||||
name: "MapVectorExchangePanel",
|
||||
props: {
|
||||
disabled: { type: Boolean, default: false },
|
||||
hasFeatures: { type: Boolean, default: false },
|
||||
},
|
||||
emits: ["import-features", "export-geojson", "export-kml", "export-kmz", "import-error"],
|
||||
data() {
|
||||
return {
|
||||
mergeImport: true,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
triggerGeoJsonPick() {
|
||||
this.$refs.geojsonInput?.click();
|
||||
},
|
||||
triggerKmlPick() {
|
||||
this.$refs.kmlInput?.click();
|
||||
},
|
||||
triggerKmzPick() {
|
||||
this.$refs.kmzInput?.click();
|
||||
},
|
||||
async readFileText(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const r = new FileReader();
|
||||
r.onload = () => resolve(String(r.result || ""));
|
||||
r.onerror = () => reject(new Error("read failed"));
|
||||
r.readAsText(file);
|
||||
});
|
||||
},
|
||||
async onGeojsonFile(ev) {
|
||||
const input = ev.target;
|
||||
const file = input.files && input.files[0];
|
||||
input.value = "";
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const text = await this.readFileText(file);
|
||||
const features = readGeoJsonToFeatures(text, "EPSG:3857");
|
||||
this.$emit("import-features", { features, merge: this.mergeImport });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.$emit("import-error", e);
|
||||
}
|
||||
},
|
||||
async onKmlFile(ev) {
|
||||
const input = ev.target;
|
||||
const file = input.files && input.files[0];
|
||||
input.value = "";
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const text = await this.readFileText(file);
|
||||
const features = readKmlToFeatures(text, "EPSG:3857");
|
||||
this.$emit("import-features", { features, merge: this.mergeImport });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.$emit("import-error", e);
|
||||
}
|
||||
},
|
||||
async readFileArrayBuffer(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const r = new FileReader();
|
||||
r.onload = () => resolve(/** @type {ArrayBuffer} */ (r.result));
|
||||
r.onerror = () => reject(new Error("read failed"));
|
||||
r.readAsArrayBuffer(file);
|
||||
});
|
||||
},
|
||||
async onKmzFile(ev) {
|
||||
const input = ev.target;
|
||||
const file = input.files && input.files[0];
|
||||
input.value = "";
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const buf = await this.readFileArrayBuffer(file);
|
||||
const features = await readKmzToFeatures(buf, "EPSG:3857");
|
||||
this.$emit("import-features", { features, merge: this.mergeImport });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.$emit("import-error", e);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
Reference in New Issue
Block a user