facilmap/frontend/src/lib/components/export-dialog.vue

355 wiersze
11 KiB
Vue

<script setup lang="ts">
import { computed, ref } from "vue";
import ModalDialog from "./ui/modal-dialog.vue";
import { getUniqueId } from "../utils/utils";
import { injectContextRequired, requireClientContext, requireMapContext } from "./facil-map-context-provider/facil-map-context-provider.vue";
import HelpPopover from "./ui/help-popover.vue";
import CopyToClipboardInput from "./ui/copy-to-clipboard-input.vue";
import type { ComponentProps } from "../utils/vue";
import type { ID } from "facilmap-types";
import validatedField from "./ui/validated-form/validated-field.vue";
import { useToasts } from "./ui/toasts/toasts.vue";
import copyToClipboard from "copy-to-clipboard";
import type { CustomSubmitEvent } from "./ui/validated-form/validated-form.vue";
import { getOrderedTypes } from "facilmap-utils";
const toasts = useToasts();
const emit = defineEmits<{
hidden: [];
}>();
const context = injectContextRequired();
const client = requireClientContext(context);
const mapContext = requireMapContext(context);
const id = getUniqueId("fm-export-map");
const orderedTypes = computed(() => getOrderedTypes(client.value.types));
const modalRef = ref<InstanceType<typeof ModalDialog>>();
const copyRef = ref<InstanceType<typeof CopyToClipboardInput>>();
const formatOptions = {
gpx: "GPX",
geojson: "GeoJSON",
table: "HTML",
csv: "CSV"
};
const hideOptions = computed(() => new Set([
"Name",
"Position",
"Distance",
"Line time",
// TODO: Include only types not currently filtered
...orderedTypes.value.flatMap((type) => type.fields.map((field) => field.name))
]));
const routeTypeOptions = {
"tracks": "Track points",
"zip": "Track points, one file per line (ZIP file)",
"routes": "Route points"
};
const format = ref<keyof typeof formatOptions>("gpx");
const routeType = ref<keyof typeof routeTypeOptions>("tracks");
const filter = ref(true);
const hide = ref(new Set<string>());
const typeId = ref<ID>();
const methodOptions = computed(() => ({
download: format.value === "table" ? "Open file" : "Download file",
link: "Generate link",
...(format.value === "table" ? {
copy: "Copy to clipboard"
} : {})
}));
const rawMethod = ref<keyof typeof methodOptions["value"]>();
const method = computed({
get: () => (rawMethod.value && Object.keys(methodOptions.value).includes(rawMethod.value)) ? rawMethod.value : (Object.keys(methodOptions.value) as Array<keyof typeof methodOptions["value"]>)[0],
set: (method) => {
rawMethod.value = method;
}
});
const resolveTypeId = (typeId: ID | undefined) => typeId != null && client.value.types[typeId] ? typeId : undefined;
const resolvedTypeId = computed(() => resolveTypeId(typeId.value));
const canSelectRouteType = computed(() => format.value === "gpx");
const canSelectType = computed(() => format.value === "csv" || (format.value === "table" && method.value === "copy"));
const mustSelectType = computed(() => canSelectType.value);
const canSelectHide = computed(() => ["table", "csv"].includes(format.value));
const validateImmediate = computed(() => method.value === "link"); // No submit button
function validateTypeId(typeId: ID | undefined) {
if (mustSelectType.value && resolveTypeId(typeId) == null) {
return "Please select a type.";
}
}
const url = computed(() => {
const params = new URLSearchParams();
if (canSelectRouteType.value) {
params.set("useTracks", routeType.value === "routes" ? "0" : "1");
}
if (canSelectHide.value && hide.value.size > 0) {
params.set("hide", [...hide.value].join(","));
}
if (filter.value && mapContext.value.filter) {
params.set("filter", mapContext.value.filter);
}
const paramsStr = params.toString();
switch (format.value) {
case "table": {
if (method.value === "copy") {
if (resolvedTypeId.value == null) {
return undefined;
}
return (
context.baseUrl
+ client.value.padData!.id
+ `/rawTable`
+ `/${resolvedTypeId.value}`
+ (paramsStr ? `?${paramsStr}` : '')
);
} else {
return (
context.baseUrl
+ client.value.padData!.id
+ `/table`
+ (paramsStr ? `?${paramsStr}` : '')
);
}
}
case "csv": {
if (resolvedTypeId.value == null) {
return undefined;
}
return (
context.baseUrl
+ client.value.padData!.id
+ `/csv`
+ `/${resolvedTypeId.value}`
+ (paramsStr ? `?${paramsStr}` : '')
);
}
case "gpx": {
return (
context.baseUrl
+ client.value.padData!.id
+ `/${format.value}`
+ (routeType.value === "zip" ? `/zip` : "")
+ (paramsStr ? `?${paramsStr}` : '')
);
}
default: {
return (
context.baseUrl
+ client.value.padData!.id
+ `/${format.value}`
+ (paramsStr ? `?${paramsStr}` : '')
);
}
}
});
const modalProps = computed((): Partial<ComponentProps<typeof ModalDialog>> => {
if (method.value === "download") {
return {
action: url.value,
target: format.value === "table" ? "_blank" : undefined,
isCreate: true,
okLabel: "Export"
};
} else if (method.value === "copy") {
return {
isCreate: true,
okLabel: "Copy"
};
} else {
return {
isCreate: false,
okVariant: "secondary"
};
}
});
function handleSubmit(e: CustomSubmitEvent): void {
if (method.value === "copy") {
e.preventDefault();
const fetchUrl = url.value;
if (fetchUrl) {
e.waitUntil((async () => {
const res = await fetch(fetchUrl);
const html = await res.text();
copyToClipboard(html, { format: "text/html" });
toasts.showToast(undefined, `${formatOptions[format.value]} export copied`, `The ${formatOptions[format.value]} export was copied to the clipboard.`, { variant: "success", autoHide: true });
})());
}
}
}
</script>
<template>
<ModalDialog
title="Export collaborative map"
size="lg"
class="fm-export-dialog"
ref="modalRef"
v-bind="modalProps"
@submit="handleSubmit"
@hidden="emit('hidden')"
>
<p>Export your map here to transfer it to another application, another device or another collaborative map.</p>
<div class="row mb-3">
<label class="col-sm-3 col-form-label" :for="`${id}-format-select`">
Format
<HelpPopover>
<p>
<strong>GPX</strong> files can be used to transfer your map data into navigation and route planning software and devices, such as
OsmAnd or Garmin. They contain your markers and lines with their names and descriptions, but not their style
attributes (with the exception of some basic attributes supported by OsmAnd).
</p>
<p>
<strong>GeoJSON</strong> files can be used to create complete backups or copies of your map. They contain the complete data of your
map, including the map settings, views, types, markers and lines along with all their data attributes. To restore
a GeoJSON backup or to create a copy of your map, simply import the file into FacilMap again.
</p>
<p>
<strong>HTML</strong> files can be opened by any web browser. Exporting a map to HTML will render a table with only the data
attributes of all markers and lines. This table can also be copy&pasted into a spreadsheet application for
further processing.
</p>
<p>
<strong>CSV</strong> files can be imported into most spreadsheet applications and only contain the data attributes of the objects
one type of marker or line.
</p>
</HelpPopover>
</label>
<div class="col-sm-9">
<select class="form-select" v-model="format" :id="`${id}-format-select`">
<option v-for="(label, value) in formatOptions" :value="value" :key="value">{{label}}</option>
</select>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-3 col-form-label" :for="`${id}-method`">Export method</label>
<div class="col-sm-9">
<select class="form-select" v-model="method" :id="`${id}-method`">
<option v-for="(label, value) in methodOptions" :value="value" :key="value">{{label}}</option>
</select>
</div>
</div>
<div v-if="canSelectRouteType" class="row mb-3">
<label class="col-sm-3 col-form-label" :for="`${id}-route-type-select`">
Route type
<HelpPopover>
<p>
<strong>Track points</strong> will export your lines exactly as they are on your map.
</p>
<p>
<strong>Track points, one file per line (ZIP file)</strong> will create a ZIP file with one GPX file
for all markers and one GPX file for each line. This works better with apps such as OsmAnd that only
support one line style per file.
</p>
<p>
<strong>Route points</strong> will export only the from/via/to route points of your lines, and your
navigation software/device will have to calculate the route using its own map data and algorithm.
</p>
</HelpPopover>
</label>
<div class="col-sm-9">
<select class="form-select" v-model="routeType" :id="`${id}-route-type-select`">
<option v-for="(label, value) in routeTypeOptions" :value="value" :key="value">{{label}}</option>
</select>
</div>
</div>
<div v-if="canSelectType" class="row mb-3">
<label class="col-sm-3 col-form-label" :for="`${id}-type-select`">
Type
</label>
<validatedField
:value="typeId"
:validators="[
validateTypeId
]"
class="col-sm-9 position-relative"
:immediate="validateImmediate"
>
<template #default="slotProps">
<select class="form-select" v-model="typeId" :id="`${id}-type-select`" :ref="slotProps.inputRef">
<option v-for="type of orderedTypes" :key="type.id" :value="type.id">{{type.name}}</option>
</select>
<div class="invalid-tooltip">
{{slotProps.validationError}}
</div>
</template>
</ValidatedField>
</div>
<div v-if="canSelectHide" class="row mb-3">
<label class="col-sm-3 col-form-label">Include columns</label>
<div class="col-sm-9 fm-export-dialog-hide-options">
<template v-for="key in hideOptions" :key="key">
<div class="form-check fm-form-check-with-label">
<input
class="form-check-input"
type="checkbox"
:id="`${id}-show-${key}-checkbox`"
:checked="!hide.has(key)"
@change="hide.has(key) ? hide.delete(key) : hide.add(key)"
>
<label class="form-check-label" :for="`${id}-show-${key}-checkbox`">{{key}}</label>
</div>
</template>
</div>
</div>
<div v-if="mapContext.filter" class="row mb-3">
<label class="col-sm-3 col-form-label" :for="`${id}-filter-checkbox`">Apply filter</label>
<div class="col-sm-9">
<div class="form-check fm-form-check-with-label">
<input
class="form-check-input"
type="checkbox"
:id="`${id}-filter-checkbox`"
v-model="filter"
>
<label class="form-check-label" :for="`${id}-filter-checkbox`">Only include objects visible under current filter</label>
</div>
</div>
</div>
<template v-if="method === 'link' && url != null">
<hr />
<CopyToClipboardInput
:modelValue="url"
readonly
ref="copyRef"
variant="primary"
></CopyToClipboardInput>
</template>
</ModalDialog>
</template>
<style lang="scss">
.fm-export-dialog {
.fm-export-dialog-hide-options {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
}
}
</style>