Files
directus/app/src/layouts/map/components/map.vue
Rijk van Zanten c48309ab68 Last eslint tweak (#18198)
* Should be there now

* Format
2023-04-14 17:40:50 -04:00

384 lines
10 KiB
Vue

<template>
<div
id="map-container"
ref="container"
:class="{ select: selectMode, hover: hoveredFeature || hoveredCluster }"
></div>
</template>
<script lang="ts">
import 'maplibre-gl/dist/maplibre-gl.css';
import maplibre, {
MapboxGeoJSONFeature,
MapLayerMouseEvent,
NavigationControl,
GeolocateControl,
LngLatBoundsLike,
GeoJSONSource,
CameraOptions,
LngLatLike,
AnyLayer,
Map,
AttributionControl,
} from 'maplibre-gl';
import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder';
import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css';
import { ref, watch, PropType, onMounted, onUnmounted, defineComponent, toRefs, computed, WatchStopHandle } from 'vue';
import { useI18n } from 'vue-i18n';
import { ShowSelect } from '@directus/types';
import { useAppStore } from '@/stores/app';
import { useSettingsStore } from '@/stores/settings';
import { BoxSelectControl, ButtonControl } from '@/utils/geometry/controls';
import { getBasemapSources, getStyleFromBasemapSource } from '@/utils/geometry/basemap';
export default defineComponent({
components: {},
props: {
data: {
type: Object as PropType<GeoJSON.FeatureCollection>,
required: true,
},
source: {
type: Object as PropType<GeoJSONSource>,
required: true,
},
layers: {
type: Array as PropType<AnyLayer[]>,
default: () => [],
},
camera: {
type: Object as PropType<CameraOptions & { bbox: any }>,
default: () => ({} as any),
},
bounds: {
type: Array as unknown as PropType<GeoJSON.BBox>,
default: undefined,
},
featureId: {
type: String,
default: undefined,
},
selection: {
type: Array as PropType<Array<string | number>>,
default: () => [],
},
showSelect: {
type: String as PropType<ShowSelect>,
default: 'multiple',
},
},
emits: ['moveend', 'featureclick', 'featureselect', 'fitdata', 'updateitempopup'],
setup(props, { emit }) {
const { t } = useI18n();
const appStore = useAppStore();
const settingsStore = useSettingsStore();
let map: Map;
const hoveredFeature = ref<MapboxGeoJSONFeature>();
const hoveredCluster = ref<boolean>();
const selectMode = ref<boolean>();
const container = ref<HTMLElement>();
const unwatchers = [] as WatchStopHandle[];
const { sidebarOpen, basemap } = toRefs(appStore);
const mapboxKey = settingsStore.settings?.mapbox_key;
const basemaps = getBasemapSources();
const style = computed(() => {
const source = basemaps.find((source) => source.name === basemap.value) ?? basemaps[0];
return getStyleFromBasemapSource(source);
});
const attributionControl = new AttributionControl();
const navigationControl = new NavigationControl({
showCompass: false,
});
const geolocateControl = new GeolocateControl();
const fitDataControl = new ButtonControl('mapboxgl-ctrl-fitdata', () => {
emit('fitdata');
});
const boxSelectControl = new BoxSelectControl({
boxElementClass: 'map-selection-box',
selectButtonClass: 'mapboxgl-ctrl-select',
layers: ['__directus_polygons', '__directus_points', '__directus_lines'],
});
let geocoderControl: MapboxGeocoder | undefined;
if (mapboxKey) {
const marker = document.createElement('div');
marker.className = 'mapboxgl-user-location-dot mapboxgl-search-location-dot';
geocoderControl = new MapboxGeocoder({
accessToken: mapboxKey,
collapsed: true,
marker: { element: marker } as any,
flyTo: { speed: 1.4 },
mapboxgl: maplibre as any,
placeholder: t('layouts.map.find_location'),
});
}
onMounted(() => {
setupMap();
});
onUnmounted(() => {
map.remove();
});
return { container, hoveredFeature, hoveredCluster, selectMode };
function setupMap() {
map = new Map({
container: 'map-container',
style: style.value,
dragRotate: false,
attributionControl: false,
...props.camera,
...(mapboxKey ? { accessToken: mapboxKey } : {}),
});
if (geocoderControl) {
map.addControl(geocoderControl as any, 'top-right');
}
map.addControl(attributionControl, 'bottom-left');
map.addControl(navigationControl, 'top-left');
map.addControl(geolocateControl, 'top-left');
map.addControl(fitDataControl, 'top-left');
map.addControl(boxSelectControl, 'top-left');
map.on('load', () => {
watch(() => style.value, updateStyle);
watch(() => props.bounds, fitBounds);
const activeLayers = ['__directus_polygons', '__directus_points', '__directus_lines'];
for (const layer of activeLayers) {
map.on('click', layer, onFeatureClick);
map.on('mousemove', layer, updatePopup);
map.on('mouseleave', layer, updatePopup);
}
map.on('move', updatePopupLocation);
map.on('click', '__directus_clusters', expandCluster);
map.on('mousemove', '__directus_clusters', hoverCluster);
map.on('mouseleave', '__directus_clusters', hoverCluster);
map.on('select.enable', () => (selectMode.value = true));
map.on('select.disable', () => (selectMode.value = false));
map.on('select.end', (event: MapLayerMouseEvent) => {
const ids = event.features?.map((f) => f.id);
emit('featureselect', { ids, replace: !event.alt });
});
map.on('moveend', () => {
emit('moveend', {
center: map.getCenter(),
zoom: map.getZoom(),
bearing: map.getBearing(),
pitch: map.getPitch(),
bbox: map.getBounds().toArray().flat(),
});
});
startWatchers();
});
watch(
() => sidebarOpen.value,
(opened) => {
if (!opened) setTimeout(() => map.resize(), 300);
}
);
setTimeout(() => map.resize(), 300);
}
function fitBounds() {
const bbox = props.data.bbox;
if (map && bbox) {
map.fitBounds(bbox as LngLatBoundsLike, {
padding: 100,
speed: 1.3,
maxZoom: 14,
});
}
}
function updateStyle(style: any) {
unwatchers.forEach((unwatch) => unwatch());
unwatchers.length = 0;
map.setStyle(style, { diff: false });
map.once('styledata', startWatchers);
}
function startWatchers() {
unwatchers.push(
watch(() => props.source, updateSource, { immediate: true }),
watch(() => props.selection, updateSelection, { immediate: true }),
watch(() => props.layers, updateLayers),
watch(() => props.data, updateData)
);
}
function updateData(newData: any) {
const source = map.getSource('__directus');
(source as GeoJSONSource).setData(newData);
updateSelection(props.selection, undefined);
}
function updateSource(newSource: GeoJSONSource) {
const layersId = new Set(map.getStyle().layers?.map(({ id }) => id));
for (const layer of props.layers) {
if (layersId.has(layer.id)) {
map.removeLayer(layer.id);
}
}
if (props.featureId) {
(newSource as any).promoteId = props.featureId;
} else {
(newSource as any).generateId = true;
}
if (map.getStyle().sources?.['__directus']) {
map.removeSource('__directus');
}
map.addSource('__directus', { ...newSource, data: props.data });
map.once('sourcedata', () => {
setTimeout(() => props.layers.forEach((layer) => map.addLayer(layer)));
});
}
function updateLayers(newLayers?: AnyLayer[], previousLayers?: AnyLayer[]) {
const currentMapLayersId = new Set(map.getStyle().layers?.map(({ id }) => id));
previousLayers?.forEach((layer) => {
if (currentMapLayersId.has(layer.id)) map.removeLayer(layer.id);
});
newLayers?.forEach((layer) => {
map.addLayer(layer);
});
}
function updateSelection(newSelection?: (string | number)[], previousSelection?: (string | number)[]) {
previousSelection?.forEach((id) => {
map.setFeatureState({ id, source: '__directus' }, { selected: false });
map.removeFeatureState({ id, source: '__directus' });
});
newSelection?.forEach((id) => {
map.setFeatureState({ id, source: '__directus' }, { selected: true });
});
}
function onFeatureClick(event: MapLayerMouseEvent) {
const feature = event.features?.[0];
const replace = props.showSelect === 'multiple' ? false : !event.originalEvent.altKey;
if (feature && props.featureId) {
if (boxSelectControl.active()) {
emit('featureselect', { ids: [feature.id], replace });
} else {
emit('featureclick', { id: feature.id, replace });
}
}
}
function updatePopup(event: MapLayerMouseEvent) {
const feature = map.queryRenderedFeatures(event.point, {
layers: ['__directus_polygons', '__directus_points', '__directus_lines'],
})[0];
const previousId = hoveredFeature.value?.id;
const featureChanged = previousId !== feature?.id;
if (previousId && featureChanged) {
map.setFeatureState({ id: previousId, source: '__directus' }, { hovered: false });
}
if (feature && feature.properties) {
if (feature.geometry.type === 'Point') {
const { x, y } = map.project(feature.geometry.coordinates as LngLatLike);
const rect = map.getContainer().getBoundingClientRect();
emit('updateitempopup', { position: { x: rect.x + x, y: rect.y + y } });
} else {
const { clientX: x, clientY: y } = event.originalEvent;
emit('updateitempopup', { position: { x, y } });
}
if (featureChanged) {
map.setFeatureState({ id: feature.id, source: '__directus' }, { hovered: true });
hoveredFeature.value = feature;
emit('updateitempopup', { item: feature.id });
}
} else {
if (featureChanged) {
hoveredFeature.value = feature;
emit('updateitempopup', { item: null });
}
}
}
function updatePopupLocation(event: MapLayerMouseEvent) {
if (hoveredFeature.value && event.originalEvent) {
const { x, y } = event.originalEvent;
emit('updateitempopup', { position: { x, y } });
}
}
function expandCluster(event: MapLayerMouseEvent) {
const features = map.queryRenderedFeatures(event.point, {
layers: ['__directus_clusters'],
});
const clusterId = features[0]?.properties?.cluster_id;
const source = map.getSource('__directus') as GeoJSONSource;
source.getClusterExpansionZoom(clusterId, (err: any, zoom: number) => {
if (err) return;
map.flyTo({
center: (features[0].geometry as GeoJSON.Point).coordinates as LngLatLike,
zoom: zoom,
speed: 1.3,
});
});
}
function hoverCluster(event: MapLayerMouseEvent) {
if (event.type == 'mousemove') {
hoveredCluster.value = true;
} else {
hoveredCluster.value = false;
}
}
},
});
</script>
<style lang="scss" scoped>
#map-container.hover :deep(.mapboxgl-canvas-container) {
cursor: pointer !important;
}
#map-container.select :deep(.mapboxgl-canvas-container) {
cursor: crosshair !important;
}
#map-container {
position: relative;
width: 100%;
height: 100%;
}
</style>