mirror of
https://github.com/directus/directus.git
synced 2026-04-03 03:00:39 -04:00
script[setup]: interfaces/map (#18424)
This commit is contained in:
@@ -61,7 +61,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import MapboxDraw from '@mapbox/mapbox-gl-draw';
|
||||
import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css';
|
||||
import maplibre, {
|
||||
@@ -75,7 +75,7 @@ import maplibre, {
|
||||
NavigationControl,
|
||||
} from 'maplibre-gl';
|
||||
import 'maplibre-gl/dist/maplibre-gl.css';
|
||||
import { computed, defineComponent, onMounted, onUnmounted, PropType, ref, toRefs, watch } from 'vue';
|
||||
import { computed, onMounted, onUnmounted, ref, toRefs, watch } from 'vue';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
import { flatten, getBBox, getGeometryFormatForType, getParser, getSerializer } from '@/utils/geometry';
|
||||
import { ButtonControl } from '@/utils/geometry/controls';
|
||||
@@ -87,6 +87,9 @@ import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css';
|
||||
import { Geometry } from 'geojson';
|
||||
import { debounce, isEqual, snakeCase } from 'lodash';
|
||||
import { getMapStyle } from './style';
|
||||
import { useAppStore } from '@/stores/app';
|
||||
import { TranslateResult, useI18n } from 'vue-i18n';
|
||||
import { getBasemapSources, getStyleFromBasemapSource } from '@/utils/geometry/basemap';
|
||||
|
||||
const activeLayers = [
|
||||
'directus-point',
|
||||
@@ -97,401 +100,364 @@ const activeLayers = [
|
||||
'directus-polygon-and-line-vertex',
|
||||
].flatMap((name) => [name + '.hot', name + '.cold']);
|
||||
|
||||
import { useAppStore } from '@/stores/app';
|
||||
import { TranslateResult, useI18n } from 'vue-i18n';
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
value: Record<string, unknown> | unknown[] | string | null;
|
||||
type: 'geometry' | 'json' | 'csv' | 'string' | 'text';
|
||||
fieldData?: Field;
|
||||
loading?: boolean;
|
||||
disabled?: boolean;
|
||||
geometryType?: GeometryType;
|
||||
defaultView?: Record<string, unknown>;
|
||||
}>(),
|
||||
{
|
||||
loading: true,
|
||||
defaultView: () => ({}),
|
||||
}
|
||||
);
|
||||
|
||||
import { getBasemapSources, getStyleFromBasemapSource } from '@/utils/geometry/basemap';
|
||||
const emit = defineEmits<{
|
||||
(e: 'input', value: Record<string, unknown> | unknown[] | string | null): void;
|
||||
}>();
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
type: {
|
||||
type: String as PropType<'geometry' | 'json' | 'csv' | 'string' | 'text'>,
|
||||
default: null,
|
||||
},
|
||||
fieldData: {
|
||||
type: Object as PropType<Field | undefined>,
|
||||
default: undefined,
|
||||
},
|
||||
value: {
|
||||
type: [Object, Array, String] as PropType<any>,
|
||||
default: null,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
geometryType: {
|
||||
type: String as PropType<GeometryType>,
|
||||
default: undefined,
|
||||
},
|
||||
defaultView: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
emits: ['input'],
|
||||
setup(props, { emit }) {
|
||||
const { t } = useI18n();
|
||||
const container = ref<HTMLElement | null>(null);
|
||||
let map: Map;
|
||||
let mapLoading = ref(true);
|
||||
let currentGeometry: Geometry | null | undefined;
|
||||
const { t } = useI18n();
|
||||
const container = ref<HTMLElement | null>(null);
|
||||
let map: Map;
|
||||
let mapLoading = ref(true);
|
||||
let currentGeometry: Geometry | null | undefined;
|
||||
|
||||
const geometryOptionsError = ref<string | null>();
|
||||
const geometryParsingError = ref<string | TranslateResult>();
|
||||
const geometryOptionsError = ref<string | null>();
|
||||
const geometryParsingError = ref<string | TranslateResult>();
|
||||
|
||||
const geometryType = props.fieldData?.type.split('.')[1] as GeometryType;
|
||||
const geometryFormat = getGeometryFormatForType(props.type)!;
|
||||
const geometryType = props.fieldData?.type.split('.')[1] as GeometryType;
|
||||
const geometryFormat = getGeometryFormatForType(props.type)!;
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
const mapboxKey = settingsStore.settings?.mapbox_key;
|
||||
const settingsStore = useSettingsStore();
|
||||
const mapboxKey = settingsStore.settings?.mapbox_key;
|
||||
|
||||
const basemaps = getBasemapSources();
|
||||
const appStore = useAppStore();
|
||||
const { basemap } = toRefs(appStore);
|
||||
const basemaps = getBasemapSources();
|
||||
const appStore = useAppStore();
|
||||
const { basemap } = toRefs(appStore);
|
||||
|
||||
const style = computed(() => {
|
||||
const source = basemaps.find((source) => source.name == basemap.value) ?? basemaps[0];
|
||||
return basemap.value, getStyleFromBasemapSource(source);
|
||||
});
|
||||
|
||||
let parse: GeoJSONParser;
|
||||
let serialize: GeoJSONSerializer;
|
||||
|
||||
try {
|
||||
parse = getParser({ geometryFormat, geometryField: 'value' });
|
||||
serialize = getSerializer({ geometryFormat, geometryField: 'value' });
|
||||
} catch (error: any) {
|
||||
geometryOptionsError.value = error;
|
||||
}
|
||||
|
||||
const selection = ref<GeoJSON.Feature[]>([]);
|
||||
|
||||
const location = ref<LngLatLike | null>();
|
||||
const projection = ref<{ x: number; y: number } | null>();
|
||||
|
||||
function updateProjection() {
|
||||
projection.value = !location.value ? null : map.project(location.value as any);
|
||||
}
|
||||
|
||||
watch(location, updateProjection);
|
||||
|
||||
const controls = {
|
||||
attribution: new AttributionControl(),
|
||||
draw: new MapboxDraw(getDrawOptions(geometryType)),
|
||||
fitData: new ButtonControl('mapboxgl-ctrl-fitdata', fitDataBounds),
|
||||
navigation: new NavigationControl({
|
||||
showCompass: false,
|
||||
}),
|
||||
geolocate: new GeolocateControl({
|
||||
showUserLocation: false,
|
||||
}),
|
||||
geocoder: undefined as MapboxGeocoder | undefined,
|
||||
};
|
||||
|
||||
if (mapboxKey) {
|
||||
controls.geocoder = new MapboxGeocoder({
|
||||
accessToken: mapboxKey,
|
||||
collapsed: true,
|
||||
flyTo: { speed: 1.4 },
|
||||
marker: false,
|
||||
mapboxgl: maplibre as any,
|
||||
placeholder: t('layouts.map.find_location'),
|
||||
});
|
||||
}
|
||||
|
||||
const tooltipVisible = ref(false);
|
||||
const tooltipMessage = ref('');
|
||||
const tooltipPosition = ref({ x: 0, y: 0 });
|
||||
|
||||
function hideTooltip() {
|
||||
tooltipVisible.value = false;
|
||||
}
|
||||
|
||||
const updateTooltipDebounce = debounce((event: any) => {
|
||||
const feature = event.features?.[0];
|
||||
|
||||
if (feature && feature.properties!.active === 'false') {
|
||||
tooltipMessage.value = t('interfaces.map.click_to_select', { geometry: feature.geometry.type });
|
||||
tooltipVisible.value = true;
|
||||
tooltipPosition.value = event.point;
|
||||
}
|
||||
}, 200);
|
||||
|
||||
function updateTooltip(event: any) {
|
||||
tooltipVisible.value = false;
|
||||
updateTooltipDebounce({ point: event.point, features: event.features });
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const cleanup = setupMap();
|
||||
onUnmounted(cleanup);
|
||||
});
|
||||
|
||||
return {
|
||||
t,
|
||||
tooltipPosition,
|
||||
tooltipVisible,
|
||||
tooltipMessage,
|
||||
container,
|
||||
mapLoading,
|
||||
resetValue,
|
||||
geometryParsingError,
|
||||
geometryOptionsError,
|
||||
basemaps,
|
||||
basemap,
|
||||
location,
|
||||
projection,
|
||||
selection,
|
||||
};
|
||||
|
||||
function setupMap(): () => void {
|
||||
map = new Map({
|
||||
container: container.value!,
|
||||
style: style.value,
|
||||
dragRotate: false,
|
||||
logoPosition: 'bottom-left',
|
||||
attributionControl: false,
|
||||
...props.defaultView,
|
||||
...(mapboxKey ? { accessToken: mapboxKey } : {}),
|
||||
});
|
||||
|
||||
if (controls.geocoder) {
|
||||
map.addControl(controls.geocoder as any, 'top-right');
|
||||
|
||||
controls.geocoder.on('result', (event: any) => {
|
||||
location.value = event.result.center;
|
||||
});
|
||||
|
||||
controls.geocoder.on('clear', () => {
|
||||
location.value = null;
|
||||
});
|
||||
}
|
||||
|
||||
controls.geolocate.on('geolocate', (event: any) => {
|
||||
const { longitude, latitude } = event.coords;
|
||||
location.value = [longitude, latitude];
|
||||
});
|
||||
|
||||
map.addControl(controls.attribution, 'bottom-left');
|
||||
map.addControl(controls.navigation, 'top-left');
|
||||
map.addControl(controls.geolocate, 'top-left');
|
||||
map.addControl(controls.fitData, 'top-left');
|
||||
map.addControl(controls.draw as any, 'top-left');
|
||||
|
||||
map.on('load', async () => {
|
||||
map.resize();
|
||||
mapLoading.value = false;
|
||||
map.on('draw.create', handleDrawUpdate);
|
||||
map.on('draw.delete', handleDrawUpdate);
|
||||
map.on('draw.update', handleDrawUpdate);
|
||||
map.on('draw.modechange', handleDrawModeChange);
|
||||
map.on('draw.selectionchange', handleSelectionChange);
|
||||
map.on('move', updateProjection);
|
||||
|
||||
for (const layer of activeLayers) {
|
||||
map.on('mousedown', layer, hideTooltip);
|
||||
map.on('mousemove', layer, updateTooltip);
|
||||
map.on('mouseleave', layer, updateTooltip);
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
});
|
||||
|
||||
watch(() => props.value, updateValue, { immediate: true });
|
||||
watch(() => style.value, updateStyle);
|
||||
watch(() => props.disabled, updateStyle);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
map.remove();
|
||||
};
|
||||
}
|
||||
|
||||
function updateValue(value: any) {
|
||||
if (!value) {
|
||||
controls.draw.deleteAll();
|
||||
currentGeometry = null;
|
||||
|
||||
if (geometryType) {
|
||||
const snaked = snakeCase(geometryType.replace('Multi', ''));
|
||||
const mode = `draw_${snaked}` as any;
|
||||
controls.draw.changeMode(mode);
|
||||
}
|
||||
} else {
|
||||
if (!isEqual(value, currentGeometry && serialize(currentGeometry))) {
|
||||
loadValueFromProps();
|
||||
controls.draw.changeMode('simple_select');
|
||||
}
|
||||
}
|
||||
|
||||
if (props.disabled) {
|
||||
controls.draw.changeMode('static');
|
||||
}
|
||||
}
|
||||
|
||||
function updateStyle() {
|
||||
map.removeControl(controls.draw as any);
|
||||
map.setStyle(style.value, { diff: false });
|
||||
controls.draw = new MapboxDraw(getDrawOptions(geometryType));
|
||||
map.addControl(controls.draw as any, 'top-left');
|
||||
loadValueFromProps();
|
||||
}
|
||||
|
||||
function resetValue(hard: boolean) {
|
||||
geometryParsingError.value = undefined;
|
||||
if (hard) emit('input', null);
|
||||
}
|
||||
|
||||
function fitDataBounds(options: CameraOptions & AnimationOptions) {
|
||||
if (map && currentGeometry) {
|
||||
const bbox = getBBox(currentGeometry);
|
||||
|
||||
map.fitBounds(bbox as LngLatBoundsLike, {
|
||||
padding: 80,
|
||||
maxZoom: 8,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getDrawOptions(type: GeometryType): any {
|
||||
const options = {
|
||||
styles: getMapStyle(),
|
||||
controls: {},
|
||||
userProperties: true,
|
||||
displayControlsDefault: false,
|
||||
modes: Object.assign(MapboxDraw.modes, {
|
||||
static: StaticMode,
|
||||
}),
|
||||
} as any;
|
||||
|
||||
if (props.disabled) {
|
||||
return options;
|
||||
}
|
||||
|
||||
if (!type) {
|
||||
options.controls.line_string = true;
|
||||
options.controls.polygon = true;
|
||||
options.controls.point = true;
|
||||
options.controls.trash = true;
|
||||
return options;
|
||||
} else {
|
||||
const base = snakeCase(type!.replace('Multi', ''));
|
||||
options.controls[base] = true;
|
||||
options.controls.trash = true;
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
||||
function isTypeCompatible(a?: GeometryType, b?: GeometryType): boolean {
|
||||
if (!a || !b) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (a.startsWith('Multi')) {
|
||||
return a.replace('Multi', '') == b.replace('Multi', '');
|
||||
}
|
||||
|
||||
return a == b;
|
||||
}
|
||||
|
||||
function loadValueFromProps() {
|
||||
try {
|
||||
controls.draw.deleteAll();
|
||||
const initialValue = parse(props);
|
||||
|
||||
if (!initialValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!props.disabled && !isTypeCompatible(geometryType, initialValue!.type)) {
|
||||
geometryParsingError.value = t('interfaces.map.unexpected_geometry', {
|
||||
expected: geometryType,
|
||||
got: initialValue!.type,
|
||||
});
|
||||
}
|
||||
|
||||
const flattened = flatten(initialValue);
|
||||
|
||||
for (const geometry of flattened) {
|
||||
controls.draw.add(geometry);
|
||||
}
|
||||
|
||||
currentGeometry = getCurrentGeometry();
|
||||
currentGeometry!.bbox = getBBox(currentGeometry!);
|
||||
|
||||
if (geometryParsingError.value) {
|
||||
const bbox = getBBox(initialValue!) as LngLatBoundsLike;
|
||||
map.fitBounds(bbox, { padding: 0, maxZoom: 8, duration: 0 });
|
||||
} else {
|
||||
fitDataBounds({ duration: 0 });
|
||||
}
|
||||
} catch (error: any) {
|
||||
geometryParsingError.value = error;
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrentGeometry(): Geometry | null {
|
||||
const features = controls.draw.getAll().features;
|
||||
const geometries = features.map((f) => f.geometry) as (SimpleGeometry | MultiGeometry)[];
|
||||
let result: Geometry;
|
||||
|
||||
if (geometries.length == 0) {
|
||||
return null;
|
||||
} else if (!geometryType) {
|
||||
if (geometries.length > 1) {
|
||||
result = { type: 'GeometryCollection', geometries };
|
||||
} else {
|
||||
result = geometries[0];
|
||||
}
|
||||
} else if (geometryType.startsWith('Multi')) {
|
||||
const coordinates = geometries
|
||||
.filter(({ type }) => `Multi${type}` == geometryType)
|
||||
.map(({ coordinates }) => coordinates);
|
||||
|
||||
result = { type: geometryType, coordinates } as Geometry;
|
||||
} else {
|
||||
result = geometries[geometries.length - 1];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function handleDrawModeChange(event: any) {
|
||||
if (!props.disabled && event.mode.startsWith('draw') && geometryType && !geometryType.startsWith('Multi')) {
|
||||
for (const feature of controls.draw.getAll().features.slice(0, -1)) {
|
||||
controls.draw.delete(feature.id as string);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelectionChange(event: any) {
|
||||
selection.value = event.features;
|
||||
}
|
||||
|
||||
function handleDrawUpdate() {
|
||||
currentGeometry = getCurrentGeometry();
|
||||
|
||||
if (!currentGeometry) {
|
||||
controls.draw.deleteAll();
|
||||
emit('input', null);
|
||||
} else {
|
||||
emit('input', serialize(currentGeometry));
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyDown(event: any) {
|
||||
if ([8, 46].includes(event.keyCode)) {
|
||||
controls.draw.trash();
|
||||
}
|
||||
}
|
||||
},
|
||||
const style = computed(() => {
|
||||
const source = basemaps.find((source) => source.name == basemap.value) ?? basemaps[0];
|
||||
return basemap.value, getStyleFromBasemapSource(source);
|
||||
});
|
||||
|
||||
let parse: GeoJSONParser;
|
||||
let serialize: GeoJSONSerializer;
|
||||
|
||||
try {
|
||||
parse = getParser({ geometryFormat, geometryField: 'value' });
|
||||
serialize = getSerializer({ geometryFormat, geometryField: 'value' });
|
||||
} catch (error: any) {
|
||||
geometryOptionsError.value = error;
|
||||
}
|
||||
|
||||
const selection = ref<GeoJSON.Feature[]>([]);
|
||||
|
||||
const location = ref<LngLatLike | null>();
|
||||
const projection = ref<{ x: number; y: number } | null>();
|
||||
|
||||
function updateProjection() {
|
||||
projection.value = !location.value ? null : map.project(location.value as any);
|
||||
}
|
||||
|
||||
watch(location, updateProjection);
|
||||
|
||||
const controls = {
|
||||
attribution: new AttributionControl(),
|
||||
draw: new MapboxDraw(getDrawOptions(geometryType)),
|
||||
fitData: new ButtonControl('mapboxgl-ctrl-fitdata', fitDataBounds),
|
||||
navigation: new NavigationControl({
|
||||
showCompass: false,
|
||||
}),
|
||||
geolocate: new GeolocateControl({
|
||||
showUserLocation: false,
|
||||
}),
|
||||
geocoder: undefined as MapboxGeocoder | undefined,
|
||||
};
|
||||
|
||||
if (mapboxKey) {
|
||||
controls.geocoder = new MapboxGeocoder({
|
||||
accessToken: mapboxKey,
|
||||
collapsed: true,
|
||||
flyTo: { speed: 1.4 },
|
||||
marker: false,
|
||||
mapboxgl: maplibre as any,
|
||||
placeholder: t('layouts.map.find_location'),
|
||||
});
|
||||
}
|
||||
|
||||
const tooltipVisible = ref(false);
|
||||
const tooltipMessage = ref('');
|
||||
const tooltipPosition = ref({ x: 0, y: 0 });
|
||||
|
||||
function hideTooltip() {
|
||||
tooltipVisible.value = false;
|
||||
}
|
||||
|
||||
const updateTooltipDebounce = debounce((event: any) => {
|
||||
const feature = event.features?.[0];
|
||||
|
||||
if (feature && feature.properties!.active === 'false') {
|
||||
tooltipMessage.value = t('interfaces.map.click_to_select', { geometry: feature.geometry.type });
|
||||
tooltipVisible.value = true;
|
||||
tooltipPosition.value = event.point;
|
||||
}
|
||||
}, 200);
|
||||
|
||||
function updateTooltip(event: any) {
|
||||
tooltipVisible.value = false;
|
||||
updateTooltipDebounce({ point: event.point, features: event.features });
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const cleanup = setupMap();
|
||||
onUnmounted(cleanup);
|
||||
});
|
||||
|
||||
function setupMap(): () => void {
|
||||
map = new Map({
|
||||
container: container.value!,
|
||||
style: style.value,
|
||||
dragRotate: false,
|
||||
logoPosition: 'bottom-left',
|
||||
attributionControl: false,
|
||||
...props.defaultView,
|
||||
...(mapboxKey ? { accessToken: mapboxKey } : {}),
|
||||
});
|
||||
|
||||
if (controls.geocoder) {
|
||||
map.addControl(controls.geocoder as any, 'top-right');
|
||||
|
||||
controls.geocoder.on('result', (event: any) => {
|
||||
location.value = event.result.center;
|
||||
});
|
||||
|
||||
controls.geocoder.on('clear', () => {
|
||||
location.value = null;
|
||||
});
|
||||
}
|
||||
|
||||
controls.geolocate.on('geolocate', (event: any) => {
|
||||
const { longitude, latitude } = event.coords;
|
||||
location.value = [longitude, latitude];
|
||||
});
|
||||
|
||||
map.addControl(controls.attribution, 'bottom-left');
|
||||
map.addControl(controls.navigation, 'top-left');
|
||||
map.addControl(controls.geolocate, 'top-left');
|
||||
map.addControl(controls.fitData, 'top-left');
|
||||
map.addControl(controls.draw as any, 'top-left');
|
||||
|
||||
map.on('load', async () => {
|
||||
map.resize();
|
||||
mapLoading.value = false;
|
||||
map.on('draw.create', handleDrawUpdate);
|
||||
map.on('draw.delete', handleDrawUpdate);
|
||||
map.on('draw.update', handleDrawUpdate);
|
||||
map.on('draw.modechange', handleDrawModeChange);
|
||||
map.on('draw.selectionchange', handleSelectionChange);
|
||||
map.on('move', updateProjection);
|
||||
|
||||
for (const layer of activeLayers) {
|
||||
map.on('mousedown', layer, hideTooltip);
|
||||
map.on('mousemove', layer, updateTooltip);
|
||||
map.on('mouseleave', layer, updateTooltip);
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
});
|
||||
|
||||
watch(() => props.value, updateValue, { immediate: true });
|
||||
watch(() => style.value, updateStyle);
|
||||
watch(() => props.disabled, updateStyle);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
map.remove();
|
||||
};
|
||||
}
|
||||
|
||||
function updateValue(value: any) {
|
||||
if (!value) {
|
||||
controls.draw.deleteAll();
|
||||
currentGeometry = null;
|
||||
|
||||
if (geometryType) {
|
||||
const snaked = snakeCase(geometryType.replace('Multi', ''));
|
||||
const mode = `draw_${snaked}` as any;
|
||||
controls.draw.changeMode(mode);
|
||||
}
|
||||
} else {
|
||||
if (!isEqual(value, currentGeometry && serialize(currentGeometry))) {
|
||||
loadValueFromProps();
|
||||
controls.draw.changeMode('simple_select');
|
||||
}
|
||||
}
|
||||
|
||||
if (props.disabled) {
|
||||
controls.draw.changeMode('static');
|
||||
}
|
||||
}
|
||||
|
||||
function updateStyle() {
|
||||
map.removeControl(controls.draw as any);
|
||||
map.setStyle(style.value, { diff: false });
|
||||
controls.draw = new MapboxDraw(getDrawOptions(geometryType));
|
||||
map.addControl(controls.draw as any, 'top-left');
|
||||
loadValueFromProps();
|
||||
}
|
||||
|
||||
function resetValue(hard: boolean) {
|
||||
geometryParsingError.value = undefined;
|
||||
if (hard) emit('input', null);
|
||||
}
|
||||
|
||||
function fitDataBounds(options: CameraOptions & AnimationOptions) {
|
||||
if (map && currentGeometry) {
|
||||
const bbox = getBBox(currentGeometry);
|
||||
|
||||
map.fitBounds(bbox as LngLatBoundsLike, {
|
||||
padding: 80,
|
||||
maxZoom: 8,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getDrawOptions(type: GeometryType): any {
|
||||
const options = {
|
||||
styles: getMapStyle(),
|
||||
controls: {},
|
||||
userProperties: true,
|
||||
displayControlsDefault: false,
|
||||
modes: Object.assign(MapboxDraw.modes, {
|
||||
static: StaticMode,
|
||||
}),
|
||||
} as any;
|
||||
|
||||
if (props.disabled) {
|
||||
return options;
|
||||
}
|
||||
|
||||
if (!type) {
|
||||
options.controls.line_string = true;
|
||||
options.controls.polygon = true;
|
||||
options.controls.point = true;
|
||||
options.controls.trash = true;
|
||||
return options;
|
||||
} else {
|
||||
const base = snakeCase(type!.replace('Multi', ''));
|
||||
options.controls[base] = true;
|
||||
options.controls.trash = true;
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
||||
function isTypeCompatible(a?: GeometryType, b?: GeometryType): boolean {
|
||||
if (!a || !b) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (a.startsWith('Multi')) {
|
||||
return a.replace('Multi', '') == b.replace('Multi', '');
|
||||
}
|
||||
|
||||
return a == b;
|
||||
}
|
||||
|
||||
function loadValueFromProps() {
|
||||
try {
|
||||
controls.draw.deleteAll();
|
||||
const initialValue = parse(props);
|
||||
|
||||
if (!initialValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!props.disabled && !isTypeCompatible(geometryType, initialValue!.type)) {
|
||||
geometryParsingError.value = t('interfaces.map.unexpected_geometry', {
|
||||
expected: geometryType,
|
||||
got: initialValue!.type,
|
||||
});
|
||||
}
|
||||
|
||||
const flattened = flatten(initialValue);
|
||||
|
||||
for (const geometry of flattened) {
|
||||
controls.draw.add(geometry);
|
||||
}
|
||||
|
||||
currentGeometry = getCurrentGeometry();
|
||||
currentGeometry!.bbox = getBBox(currentGeometry!);
|
||||
|
||||
if (geometryParsingError.value) {
|
||||
const bbox = getBBox(initialValue!) as LngLatBoundsLike;
|
||||
map.fitBounds(bbox, { padding: 0, maxZoom: 8, duration: 0 });
|
||||
} else {
|
||||
fitDataBounds({ duration: 0 });
|
||||
}
|
||||
} catch (error: any) {
|
||||
geometryParsingError.value = error;
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrentGeometry(): Geometry | null {
|
||||
const features = controls.draw.getAll().features;
|
||||
const geometries = features.map((f) => f.geometry) as (SimpleGeometry | MultiGeometry)[];
|
||||
let result: Geometry;
|
||||
|
||||
if (geometries.length == 0) {
|
||||
return null;
|
||||
} else if (!geometryType) {
|
||||
if (geometries.length > 1) {
|
||||
result = { type: 'GeometryCollection', geometries };
|
||||
} else {
|
||||
result = geometries[0];
|
||||
}
|
||||
} else if (geometryType.startsWith('Multi')) {
|
||||
const coordinates = geometries
|
||||
.filter(({ type }) => `Multi${type}` == geometryType)
|
||||
.map(({ coordinates }) => coordinates);
|
||||
|
||||
result = { type: geometryType, coordinates } as Geometry;
|
||||
} else {
|
||||
result = geometries[geometries.length - 1];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function handleDrawModeChange(event: any) {
|
||||
if (!props.disabled && event.mode.startsWith('draw') && geometryType && !geometryType.startsWith('Multi')) {
|
||||
for (const feature of controls.draw.getAll().features.slice(0, -1)) {
|
||||
controls.draw.delete(feature.id as string);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelectionChange(event: any) {
|
||||
selection.value = event.features;
|
||||
}
|
||||
|
||||
function handleDrawUpdate() {
|
||||
currentGeometry = getCurrentGeometry();
|
||||
|
||||
if (!currentGeometry) {
|
||||
controls.draw.deleteAll();
|
||||
emit('input', null);
|
||||
} else {
|
||||
emit('input', serialize(currentGeometry));
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyDown(event: any) {
|
||||
if ([8, 46].includes(event.keyCode)) {
|
||||
controls.draw.trash();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="form-grid">
|
||||
<div v-if="!nativeGeometryType && field.type !== 'csv'" class="field half-left">
|
||||
<div v-if="!nativeGeometryType && field?.type !== 'csv'" class="field half-left">
|
||||
<div class="type-label">{{ t('interfaces.map.geometry_type') }}</div>
|
||||
<v-select
|
||||
v-model="geometryType"
|
||||
@@ -16,104 +16,85 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { ref, defineComponent, PropType, watch, onMounted, onUnmounted, computed, toRefs } from 'vue';
|
||||
import { GEOMETRY_TYPES } from '@directus/constants';
|
||||
import { Field, GeometryType, GeometryOptions } from '@directus/types';
|
||||
import { getBasemapSources, getStyleFromBasemapSource } from '@/utils/geometry/basemap';
|
||||
import 'maplibre-gl/dist/maplibre-gl.css';
|
||||
import { Map, CameraOptions } from 'maplibre-gl';
|
||||
<script setup lang="ts">
|
||||
import { useAppStore } from '@/stores/app';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
import { getBasemapSources, getStyleFromBasemapSource } from '@/utils/geometry/basemap';
|
||||
import { GEOMETRY_TYPES } from '@directus/constants';
|
||||
import { Field, GeometryOptions, GeometryType } from '@directus/types';
|
||||
import { CameraOptions, Map } from 'maplibre-gl';
|
||||
import 'maplibre-gl/dist/maplibre-gl.css';
|
||||
import { computed, onMounted, onUnmounted, ref, toRefs, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
collection: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
field: {
|
||||
type: Object as PropType<Field>,
|
||||
default: null,
|
||||
},
|
||||
value: {
|
||||
type: Object as PropType<GeometryOptions & { defaultView?: CameraOptions }>,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
emits: ['input'],
|
||||
setup(props, { emit }) {
|
||||
const { t } = useI18n();
|
||||
const props = defineProps<{
|
||||
collection: string;
|
||||
field?: Field;
|
||||
value?: GeometryOptions & { defaultView?: CameraOptions };
|
||||
}>();
|
||||
|
||||
const nativeGeometryType = computed(() => (props.field?.type.split('.')[1] as GeometryType) ?? 'Point');
|
||||
const geometryType = ref<GeometryType>(nativeGeometryType.value ?? props.value?.geometryType ?? 'Point');
|
||||
const defaultView = ref<CameraOptions | undefined>(props.value?.defaultView);
|
||||
const emit = defineEmits(['input']);
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
watch(() => props.field?.type, watchType);
|
||||
watch(nativeGeometryType, watchNativeType);
|
||||
watch([geometryType, defaultView], input, { immediate: true });
|
||||
const nativeGeometryType = computed(() => (props.field?.type.split('.')[1] as GeometryType) ?? 'Point');
|
||||
const geometryType = ref<GeometryType>(nativeGeometryType.value ?? props.value?.geometryType ?? 'Point');
|
||||
const defaultView = ref<CameraOptions | undefined>(props.value?.defaultView);
|
||||
|
||||
function watchType(type: string | undefined) {
|
||||
if (type === 'csv') geometryType.value = 'Point';
|
||||
}
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
function watchNativeType(type: GeometryType) {
|
||||
geometryType.value = type;
|
||||
}
|
||||
watch(() => props.field?.type, watchType);
|
||||
watch(nativeGeometryType, watchNativeType);
|
||||
watch([geometryType, defaultView], input, { immediate: true });
|
||||
|
||||
function input() {
|
||||
emit('input', {
|
||||
defaultView,
|
||||
geometryType: geometryType.value,
|
||||
});
|
||||
}
|
||||
function watchType(type: string | undefined) {
|
||||
if (type === 'csv') geometryType.value = 'Point';
|
||||
}
|
||||
|
||||
const mapContainer = ref<HTMLElement | null>(null);
|
||||
let map: Map;
|
||||
function watchNativeType(type: GeometryType) {
|
||||
geometryType.value = type;
|
||||
}
|
||||
|
||||
const mapboxKey = settingsStore.settings?.mapbox_key;
|
||||
const basemaps = getBasemapSources();
|
||||
const appStore = useAppStore();
|
||||
const { basemap } = toRefs(appStore);
|
||||
function input() {
|
||||
emit('input', {
|
||||
defaultView,
|
||||
geometryType: geometryType.value,
|
||||
});
|
||||
}
|
||||
|
||||
const style = computed(() => {
|
||||
const source = basemaps.find((source) => source.name == basemap.value) ?? basemaps[0];
|
||||
return getStyleFromBasemapSource(source);
|
||||
});
|
||||
const mapContainer = ref<HTMLElement | null>(null);
|
||||
let map: Map;
|
||||
|
||||
onMounted(() => {
|
||||
map = new Map({
|
||||
container: mapContainer.value!,
|
||||
style: style.value,
|
||||
...(defaultView.value || {}),
|
||||
...(mapboxKey ? { accessToken: mapboxKey } : {}),
|
||||
});
|
||||
const mapboxKey = settingsStore.settings?.mapbox_key;
|
||||
const basemaps = getBasemapSources();
|
||||
const appStore = useAppStore();
|
||||
const { basemap } = toRefs(appStore);
|
||||
|
||||
map.on('moveend', () => {
|
||||
defaultView.value = {
|
||||
center: map.getCenter(),
|
||||
zoom: map.getZoom(),
|
||||
bearing: map.getBearing(),
|
||||
pitch: map.getPitch(),
|
||||
};
|
||||
});
|
||||
});
|
||||
const style = computed(() => {
|
||||
const source = basemaps.find((source) => source.name == basemap.value) ?? basemaps[0];
|
||||
return getStyleFromBasemapSource(source);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
map.remove();
|
||||
});
|
||||
onMounted(() => {
|
||||
map = new Map({
|
||||
container: mapContainer.value!,
|
||||
style: style.value,
|
||||
...(defaultView.value || {}),
|
||||
...(mapboxKey ? { accessToken: mapboxKey } : {}),
|
||||
});
|
||||
|
||||
return {
|
||||
t,
|
||||
nativeGeometryType,
|
||||
GEOMETRY_TYPES,
|
||||
geometryType,
|
||||
mapContainer,
|
||||
map.on('moveend', () => {
|
||||
defaultView.value = {
|
||||
center: map.getCenter(),
|
||||
zoom: map.getZoom(),
|
||||
bearing: map.getBearing(),
|
||||
pitch: map.getPitch(),
|
||||
};
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
map.remove();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user