Show node on map in confirmation before deleting it.

This commit is contained in:
baldo 2022-08-24 23:48:03 +02:00
commit aa0d63fd44
10 changed files with 205 additions and 8 deletions

View file

@ -28,6 +28,9 @@ body {
}
main {
position: relative;
z-index: 0;
padding-top: $page-padding-top;
padding-bottom: $page-padding-bottom;

View file

@ -0,0 +1,109 @@
<script setup lang="ts">
import "leaflet/dist/leaflet.css";
import { onMounted, ref } from "vue";
import { useConfigStore } from "@/stores/config";
import type { Coordinates } from "@/types";
import * as L from "leaflet";
import { parseToFloat } from "@/utils/Numbers";
const wrapper = ref<HTMLElement>();
const configStore = useConfigStore();
interface Props {
coordinates?: Coordinates;
}
const props = defineProps<Props>();
onMounted(renderMap);
function getLayers(): {
layers: { [name: string]: L.Layer };
defaultLayers: L.Layer[];
} {
const layers: { [name: string]: L.Layer } = {};
const defaultLayers: L.Layer[] = [];
for (const [id, layerCfg] of Object.entries(
configStore.getConfig.coordsSelector.layers
)) {
const layerOptions = layerCfg.layerOptions;
const layer = L.tileLayer(layerCfg.url, {
id,
...layerOptions,
});
layers[layerCfg.name] = layer;
if (defaultLayers.length === 0) {
defaultLayers.push(layer);
}
}
return { layers, defaultLayers };
}
function createMap(defaultLayers: L.Layer[], layers: { [p: string]: L.Layer }) {
const element = wrapper.value;
if (!element) {
throw new Error("Illegal state: Map wrapper element not set.");
}
const options: L.MapOptions = {
renderer: new L.Canvas(),
minZoom: 2,
crs: L.CRS.EPSG3857,
zoomControl: true,
worldCopyJump: true,
maxBounds: L.latLngBounds([90, -540.5], [-90, 540.5]),
attributionControl: true,
layers: defaultLayers,
};
const map = L.map(element, options);
L.control.layers(layers).addTo(map);
return map;
}
function centerOnCoordinates(map: L.Map) {
let { lat, lng, defaultZoom: zoom } = configStore.getConfig.coordsSelector;
if (props.coordinates) {
[lat, lng] = props.coordinates.split(" ").map(parseToFloat);
zoom = map.getMaxZoom();
L.marker([lat, lng], {}).addTo(map);
}
map.setView([lat, lng], zoom);
}
function renderMap() {
const { layers, defaultLayers } = getLayers();
const map = createMap(defaultLayers, layers);
centerOnCoordinates(map);
}
</script>
<template>
<div class="map-container">
<div class="map" ref="wrapper"></div>
</div>
</template>
<style lang="scss">
@use "sass:math";
@import "../scss/variables";
.map-container {
position: relative;
width: 100%;
padding-top: math.percentage(math.div(1, $node-map-aspect-ratio));
.map {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: $node-map-border-radius;
}
}
</style>

View file

@ -103,6 +103,9 @@ async function onAbort() {
<style lang="scss" scoped>
.preview {
margin: 1em 0;
margin: {
top: 1em;
bottom: 1em;
}
}
</style>

View file

@ -1,6 +1,7 @@
<script setup lang="ts">
import type { StoredNode } from "@/types";
import { MonitoringState } from "@/types";
import NodeMap from "@/components/NodeMap.vue";
interface Props {
node: StoredNode;
@ -54,11 +55,21 @@ const props = defineProps<Props>();
nicht aktiv
</span>
</div>
<NodeMap class="node-map" :coordinates="props.node.coords" />
</div>
</template>
<style lang="scss" scoped>
@import "../../scss/variables";
@import "../../scss/mixins";
@include min-page-breakpoint(medium) {
.node-preview-card {
max-width: nth(map-get($page-breakpoints, small), 1);
margin: 0 auto;
}
}
.node-preview-card {
display: flex;
@ -76,6 +87,10 @@ const props = defineProps<Props>();
text-align: center;
}
.node-map {
margin: $node-preview-card-map-margin;
}
.field {
margin: $node-preview-card-field-margin;
max-width: 100%;

View file

@ -120,12 +120,17 @@ $error-card-link-color: darken($variant-color-danger, 30%);
$error-card-link-hover-color: $error-card-link-color;
$error-card-link-focus-outline: 0.1em solid $error-card-link-hover-color;
// Node map
$node-map-aspect-ratio: 16 / 10;
$node-map-border-radius: 0.75em;
// Node preview
$node-preview-card-padding: 0.75em 1em;
$node-preview-card-border: 0.2em solid $gray;
$node-preview-card-border-radius: 1em;
$node-preview-card-headline-font-size: 1.5em;
$node-preview-card-headline-margin: 0 0 0.5em;
$node-preview-card-map-margin: 1em 0 0.25em 0;
$node-preview-card-field-margin: 0.25em 0;
$node-preview-card-field-code-font-size: 1.25em;

View file

@ -1,6 +1,6 @@
import { SortDirection, toIsArray, type TypeGuard } from "@/types";
import type { Headers } from "request";
import { parseInteger } from "@/utils/Numbers";
import { parseToInteger } from "@/utils/Numbers";
type Method = "GET" | "PUT" | "POST" | "DELETE";
@ -172,7 +172,7 @@ class Api {
...filter,
});
const totalStr = response.headers.get("x-total-count");
const total = parseInteger(totalStr, 10);
const total = parseToInteger(totalStr, 10);
return {
entries: response.result,

View file

@ -1,9 +1,12 @@
export function isInteger(arg: unknown): arg is number {
return typeof arg === "number" && Number.isInteger(arg);
}
import { isNumber } from "@/shared/types";
// TODO: Write tests!
export function parseInteger(arg: unknown, radix: number): number {
export function isInteger(arg: unknown): arg is number {
return isNumber(arg) && Number.isInteger(arg);
}
export function parseToInteger(arg: unknown, radix: number): number {
if (isInteger(arg)) {
return arg;
}
@ -21,7 +24,7 @@ export function parseInteger(arg: unknown, radix: number): number {
}
if (num.toString(radix).toLowerCase() !== str.toLowerCase()) {
throw new Error(
`Parsed integer does not match given string (radix: {radix}): ${str}`
`Parsed integer does not match given string (radix: ${radix}): ${str}`
);
}
return num;
@ -30,3 +33,39 @@ export function parseInteger(arg: unknown, radix: number): number {
throw new Error(`Cannot parse number (radix: ${radix}): ${arg}`);
}
}
export function isFloat(arg: unknown): arg is number {
return isNumber(arg) && Number.isFinite(arg);
}
export function parseToFloat(arg: unknown): number {
if (isFloat(arg)) {
return arg;
}
switch (typeof arg) {
case "number":
throw new Error(`Not a finite number: ${arg}`);
case "string": {
let str = (arg as string).trim();
const num = parseFloat(str);
if (isNaN(num)) {
throw new Error(`Not a valid number: ${str}`);
}
if (Number.isInteger(num)) {
str = str.replace(/\.0+$/, "");
}
if (num.toString(10) !== str) {
throw new Error(
`Parsed float does not match given string: ${num.toString(
10
)} !== ${str}`
);
}
return num;
}
default:
throw new Error(`Cannot parse number: ${arg}`);
}
}