mirror of
https://github.com/directus/directus.git
synced 2026-01-25 21:18:31 -05:00
script[setup]: layouts/map (#18440)
This commit is contained in:
@@ -6,21 +6,18 @@
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
itemCount?: number;
|
||||
showingCount: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
itemCount: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
showingCount: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -6,364 +6,343 @@
|
||||
></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';
|
||||
<script setup lang="ts">
|
||||
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 maplibre, {
|
||||
AnyLayer,
|
||||
AttributionControl,
|
||||
CameraOptions,
|
||||
GeoJSONSource,
|
||||
GeolocateControl,
|
||||
LngLatBoundsLike,
|
||||
LngLatLike,
|
||||
Map,
|
||||
MapLayerMouseEvent,
|
||||
MapboxGeoJSONFeature,
|
||||
NavigationControl,
|
||||
} from 'maplibre-gl';
|
||||
import 'maplibre-gl/dist/maplibre-gl.css';
|
||||
import { WatchStopHandle, computed, onMounted, onUnmounted, ref, toRefs, watch } 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';
|
||||
import { BoxSelectControl, ButtonControl } from '@/utils/geometry/controls';
|
||||
import { ShowSelect } from '@directus/types';
|
||||
|
||||
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 props = withDefaults(
|
||||
defineProps<{
|
||||
data: GeoJSON.FeatureCollection;
|
||||
source: GeoJSONSource;
|
||||
layers?: AnyLayer[];
|
||||
camera?: CameraOptions & { bbox: any };
|
||||
bounds?: GeoJSON.BBox;
|
||||
featureId?: string;
|
||||
selection?: (string | number)[];
|
||||
showSelect?: ShowSelect;
|
||||
}>(),
|
||||
{
|
||||
layers: () => [],
|
||||
camera: () => ({} as any),
|
||||
selection: () => [],
|
||||
showSelect: 'multiple',
|
||||
}
|
||||
);
|
||||
|
||||
const style = computed(() => {
|
||||
const source = basemaps.find((source) => source.name === basemap.value) ?? basemaps[0];
|
||||
return getStyleFromBasemapSource(source);
|
||||
});
|
||||
const emit = defineEmits(['moveend', 'featureclick', 'featureselect', 'fitdata', 'updateitempopup']);
|
||||
|
||||
const attributionControl = new AttributionControl();
|
||||
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 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;
|
||||
}
|
||||
}
|
||||
},
|
||||
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();
|
||||
});
|
||||
|
||||
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>
|
||||
|
||||
@@ -93,132 +93,57 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
export default {
|
||||
inheritAttrs: false,
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import MapComponent from './components/map.vue';
|
||||
import { useSync } from '@directus/composables';
|
||||
import { GeometryOptions, Item } from '@directus/types';
|
||||
|
||||
export default defineComponent({
|
||||
components: { MapComponent },
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
collection: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
selection: {
|
||||
type: Array as PropType<Item[]>,
|
||||
default: () => [],
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
error: {
|
||||
type: Object as PropType<any>,
|
||||
default: null,
|
||||
},
|
||||
geojsonError: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
geometryOptions: {
|
||||
type: Object as PropType<GeometryOptions>,
|
||||
default: undefined,
|
||||
},
|
||||
geojson: {
|
||||
type: Object as PropType<any>,
|
||||
required: true,
|
||||
},
|
||||
featureId: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
geojsonBounds: {
|
||||
type: Object as PropType<any>,
|
||||
default: undefined,
|
||||
},
|
||||
directusSource: {
|
||||
type: Object as PropType<any>,
|
||||
required: true,
|
||||
},
|
||||
directusLayers: {
|
||||
type: Array as PropType<any[]>,
|
||||
required: true,
|
||||
},
|
||||
handleClick: {
|
||||
type: Function as PropType<(event: { id: string | number; replace: boolean }) => void>,
|
||||
required: true,
|
||||
},
|
||||
handleSelect: {
|
||||
type: Function as PropType<(event: { ids: Array<string | number>; replace: boolean }) => void>,
|
||||
required: true,
|
||||
},
|
||||
cameraOptions: {
|
||||
type: Object as PropType<any>,
|
||||
default: undefined,
|
||||
},
|
||||
resetPresetAndRefresh: {
|
||||
type: Function as PropType<() => Promise<void>>,
|
||||
required: true,
|
||||
},
|
||||
geojsonLoading: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
itemCount: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
totalPages: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
page: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
toPage: {
|
||||
type: Function as PropType<(newPage: number) => void>,
|
||||
required: true,
|
||||
},
|
||||
limit: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
autoLocationFilter: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
},
|
||||
fitDataBounds: {
|
||||
type: Function as PropType<() => void>,
|
||||
required: true,
|
||||
},
|
||||
template: {
|
||||
type: String,
|
||||
default: () => undefined,
|
||||
},
|
||||
itemPopup: {
|
||||
type: Object as PropType<{ item?: any; position?: { x: number; y: number } }>,
|
||||
default: () => undefined,
|
||||
},
|
||||
updateItemPopup: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ['update:cameraOptions', 'update:limit'],
|
||||
setup(props, { emit }) {
|
||||
const { t, n } = useI18n();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
collection: string;
|
||||
geojson: any;
|
||||
directusSource: any;
|
||||
directusLayers: any[];
|
||||
handleClick: (event: { id: string | number; replace: boolean }) => void;
|
||||
handleSelect: (event: { ids: Array<string | number>; replace: boolean }) => void;
|
||||
resetPresetAndRefresh: () => Promise<void>;
|
||||
fitDataBounds: () => void;
|
||||
updateItemPopup: () => void;
|
||||
geojsonLoading: boolean;
|
||||
loading: boolean;
|
||||
totalPages: number;
|
||||
page: number;
|
||||
toPage: (newPage: number) => void;
|
||||
limit: number;
|
||||
selection?: Item[];
|
||||
error?: any;
|
||||
geojsonError?: string;
|
||||
geometryOptions?: GeometryOptions;
|
||||
featureId?: string;
|
||||
geojsonBounds?: any;
|
||||
cameraOptions?: any;
|
||||
itemCount?: number;
|
||||
autoLocationFilter?: boolean;
|
||||
template?: string;
|
||||
itemPopup?: { item?: any; position?: { x: number; y: number } };
|
||||
}>(),
|
||||
{
|
||||
selection: () => [],
|
||||
}
|
||||
);
|
||||
|
||||
const cameraOptionsWritable = useSync(props, 'cameraOptions', emit);
|
||||
const limitWritable = useSync(props, 'limit', emit);
|
||||
const emit = defineEmits(['update:cameraOptions', 'update:limit']);
|
||||
|
||||
return { t, n, cameraOptionsWritable, limitWritable };
|
||||
},
|
||||
});
|
||||
const { t, n } = useI18n();
|
||||
|
||||
const cameraOptionsWritable = useSync(props, 'cameraOptions', emit);
|
||||
const limitWritable = useSync(props, 'limit', emit);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -37,64 +37,37 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { defineComponent, PropType, toRefs } from 'vue';
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useAppStore } from '@/stores/app';
|
||||
import { getBasemapSources } from '@/utils/geometry/basemap';
|
||||
import { GeometryOptions, Item } from '@directus/types';
|
||||
import { useSync } from '@directus/composables';
|
||||
import { GeometryOptions, Item } from '@directus/types';
|
||||
import { toRefs } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
export default defineComponent({
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
collection: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
geometryFields: {
|
||||
type: Array as PropType<Item[]>,
|
||||
required: true,
|
||||
},
|
||||
geometryField: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
geometryOptions: {
|
||||
type: Object as PropType<GeometryOptions>,
|
||||
default: undefined,
|
||||
},
|
||||
clusterData: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
},
|
||||
displayTemplate: {
|
||||
type: String as string | undefined,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
emits: ['update:geometryField', 'update:autoLocationFilter', 'update:clusterData'],
|
||||
setup(props, { emit }) {
|
||||
const { t } = useI18n();
|
||||
const props = defineProps<{
|
||||
collection: string;
|
||||
geometryFields: Item[];
|
||||
geometryField?: string;
|
||||
geometryOptions?: GeometryOptions;
|
||||
clusterData?: boolean;
|
||||
displayTemplate?: string;
|
||||
}>();
|
||||
|
||||
const appStore = useAppStore();
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:geometryField', geometryField: string): void;
|
||||
(e: 'update:clusterData', clusterData: boolean): void;
|
||||
(e: 'update:displayTemplate', displayTemplate: string): void;
|
||||
}>();
|
||||
|
||||
const geometryFieldWritable = useSync(props, 'geometryField', emit);
|
||||
const clusterDataWritable = useSync(props, 'clusterData', emit);
|
||||
const displayTemplateWritable = useSync(props, 'displayTemplate', emit);
|
||||
const { t } = useI18n();
|
||||
|
||||
const basemaps = getBasemapSources();
|
||||
const { basemap } = toRefs(appStore);
|
||||
const appStore = useAppStore();
|
||||
|
||||
return {
|
||||
t,
|
||||
geometryFieldWritable,
|
||||
clusterDataWritable,
|
||||
displayTemplateWritable,
|
||||
basemaps,
|
||||
basemap,
|
||||
};
|
||||
},
|
||||
});
|
||||
const geometryFieldWritable = useSync(props, 'geometryField', emit);
|
||||
const clusterDataWritable = useSync(props, 'clusterData', emit);
|
||||
const displayTemplateWritable = useSync(props, 'displayTemplate', emit);
|
||||
|
||||
const basemaps = getBasemapSources();
|
||||
const { basemap } = toRefs(appStore);
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user