diff --git a/apps/demo-draggable/.eslintrc.json b/apps/demo-draggable/.eslintrc.json index a5fbd42..c428d22 100644 --- a/apps/demo-draggable/.eslintrc.json +++ b/apps/demo-draggable/.eslintrc.json @@ -14,6 +14,10 @@ "rules": { "vue/multi-word-component-names": "off" } + }, + { + "files": ["*.html"], + "processor": "vue/.vue" } ] } diff --git a/apps/demo-draggable/index.html b/apps/demo-draggable/index.html index cd37961..70b3b3d 100644 --- a/apps/demo-draggable/index.html +++ b/apps/demo-draggable/index.html @@ -2,7 +2,7 @@ - + demo draggable diff --git a/apps/demo-manga/index.html b/apps/demo-manga/index.html index dfbfe04..7793c99 100644 --- a/apps/demo-manga/index.html +++ b/apps/demo-manga/index.html @@ -2,7 +2,7 @@ - + demo-manga diff --git a/apps/demo-map/.eslintrc.json b/apps/demo-map/.eslintrc.json index a5fbd42..c428d22 100644 --- a/apps/demo-map/.eslintrc.json +++ b/apps/demo-map/.eslintrc.json @@ -14,6 +14,10 @@ "rules": { "vue/multi-word-component-names": "off" } + }, + { + "files": ["*.html"], + "processor": "vue/.vue" } ] } diff --git a/apps/demo-map/index.html b/apps/demo-map/index.html index 3d0221a..883a682 100644 --- a/apps/demo-map/index.html +++ b/apps/demo-map/index.html @@ -2,7 +2,7 @@ - + import('../views/Draw/example.vue'), }, + { + path: '/inspect/', + component: () => import('../views/Draw/inspect.vue'), + }, { path: '/compare/', component: () => import('../views/AllMapCompareView.vue'), diff --git a/apps/demo-map/src/views/AllMapView.vue b/apps/demo-map/src/views/AllMapView.vue index 049bf06..58ef07a 100644 --- a/apps/demo-map/src/views/AllMapView.vue +++ b/apps/demo-map/src/views/AllMapView.vue @@ -11,6 +11,7 @@ import { FullScreenControl, GeoLocateControl, getMap, + GlobeControl, GotoControl, HomeControl, Map, @@ -36,24 +37,31 @@ import { createMenuItemStyleEdit, createMenuItemToBoundActionForItem, createMenuItemToBoundActionForList, + createMenuItemToggleShow, createMultiLegend, createMultiMapboxLayerComponent, DatasetComposite, findSiblingOrNearestLeaf, IdentifyControl, + IdentifyShowFirstControl, IListViewUI, isDataManagementView, isDatasetMap, LayerControl, LayerHighlight, - LayerInfoControl, LayerSimpleMapboxBuild, } from '@hungpvq/vue-map-dataset'; -import { callDraw, DrawControl, DrawingType } from '@hungpvq/vue-map-draw'; +import { + callDraw, + DrawControl, + DrawingType, + InspectControl, +} from '@hungpvq/vue-map-draw'; import { MeasurementControl } from '@hungpvq/vue-map-measurement'; +import { PrintAdvancedControl, PrintControl } from '@hungpvq/vue-map-print'; import { mdiDownload, mdiPencil } from '@mdi/js'; import { ref } from 'vue'; -const { status, error, downloadFile } = useDownloadFile(); +const { downloadFile } = useDownloadFile(); const mapRef = ref(); const { convertList } = useConvertToGeoJSON(); @@ -65,7 +73,6 @@ function onMapLoaded(map: MapSimple) { true ) as DatasetComposite; const source_raster = createDatasetPartRasterSourceComponent('source', { - name: 'raster 1', type: 'raster', tiles: [ 'https://naturalearthtiles.roblabs.com/tiles/natural_earth_cross_blended_hypso_shaded_relief.raster/{z}/{x}/{y}.png', @@ -152,11 +159,13 @@ function onMapLoaded(map: MapSimple) { .build(), ]); list1.addMenus([ + createMenuItemToggleShow({ location: 'extra' }), createMenuItemToBoundActionForList(), createMenuItemShowDetailInfoSource(), createMenuItemStyleEdit(), ]); list2.addMenus([ + createMenuItemToggleShow({ location: 'extra' }), createMenuItemToBoundActionForList(), createMenuDrawLayer(), createMenuDownload(), @@ -252,8 +261,56 @@ function onMapLoaded(map: MapSimple) { dataset.add(identify); dataset.add(metadata); addDataset(map.id, dataset); + addDataset(map.id, createDatasetPoint()); // addDataset(map.id, dataset_raster); } +function createDatasetPoint() { + const dataset = createDataset('Group test', null, true) as DatasetComposite; + const source = createDatasetPartGeojsonSourceComponent('source', { + type: 'FeatureCollection', + features: [], + }); + const groupLayer1 = createDataset( + 'Group layer 1', + null, + true + ) as DatasetComposite; + const list1: IListViewUI = createDatasetPartListViewUiComponent('test point'); + const layer1 = createMultiMapboxLayerComponent('layer area', [ + new LayerSimpleMapboxBuild() + .setStyleType('point') + .setColor(list1.color) + .build(), + ]); + groupLayer1.add(layer1); + groupLayer1.add(list1); + list1.addMenus([ + createMenuItemToggleShow({ location: 'extra' }), + createMenuItemToBoundActionForList(), + createMenuItemShowDetailInfoSource(), + createMenuItemStyleEdit(), + ]); + const dataManagement = createDataManagementMapboxComponent( + 'data management', + { + fields: [{ text: 'Name', value: 'name' }], + } + ); + dataManagement.setItems([ + { + id: '2', + name: 'feature 2', + geometry: { + coordinates: [106.26447460804093, 20.9143362367018], + type: 'Point', + }, + }, + ]); + dataset.add(source); + dataset.add(dataManagement); + dataset.add(groupLayer1); + return dataset; +} function createMenuDownload() { return createMenuItem({ type: 'item', @@ -329,13 +386,17 @@ function createMenuDrawLayer() { --> + + + + @@ -344,6 +405,7 @@ function createMenuDrawLayer() { + diff --git a/apps/demo-map/src/views/Draw/inspect.vue b/apps/demo-map/src/views/Draw/inspect.vue new file mode 100644 index 0000000..d59ac9a --- /dev/null +++ b/apps/demo-map/src/views/Draw/inspect.vue @@ -0,0 +1,28 @@ + + + + + + diff --git a/apps/demo-map/src/views/StoryTelling/example-gps.vue b/apps/demo-map/src/views/StoryTelling/example-gps.vue index a83cb80..2f49a67 100644 --- a/apps/demo-map/src/views/StoryTelling/example-gps.vue +++ b/apps/demo-map/src/views/StoryTelling/example-gps.vue @@ -16,7 +16,7 @@ import { } from '@hungpvq/vue-map-core'; import { MeasurementControl } from '@hungpvq/vue-map-measurement'; import * as turf from '@turf/turf'; -import { GeoJSONSource, Marker } from 'mapbox-gl'; +import { GeoJSONSource, Marker } from 'maplibre-gl'; import { ref } from 'vue'; import { createZoomAction } from './helper-action'; import { withMapReady } from './helper-global'; diff --git a/apps/demo-map/src/views/StoryTelling/helper-global.ts b/apps/demo-map/src/views/StoryTelling/helper-global.ts index 964b6c4..9bb2c1e 100644 --- a/apps/demo-map/src/views/StoryTelling/helper-global.ts +++ b/apps/demo-map/src/views/StoryTelling/helper-global.ts @@ -1,5 +1,5 @@ import { getMap } from '@hungpvq/vue-map-core'; -import { GeoJSONSource } from 'mapbox-gl'; +import { GeoJSONSource, Map } from 'maplibre-gl'; import { Ref } from 'vue'; export function createOrbitGlobalActions(mapId: Ref) { @@ -104,10 +104,7 @@ export function createSimpleMapAction(mapId: Ref) { }; } -export function withMapReady( - mapId: string, - action: (map: mapboxgl.Map) => void -) { +export function withMapReady(mapId: string, action: (map: Map) => void) { getMap(mapId, (map) => { if (map.isStyleLoaded()) { action(map); diff --git a/apps/demo-map/src/views/StoryTelling/useStorytelling.ts b/apps/demo-map/src/views/StoryTelling/useStorytelling.ts index b26fa4d..30a8329 100644 --- a/apps/demo-map/src/views/StoryTelling/useStorytelling.ts +++ b/apps/demo-map/src/views/StoryTelling/useStorytelling.ts @@ -91,12 +91,9 @@ export function useStorytelling(options: UseStorytellingOptions) { const runCurrentChapter = () => { const chapter = chapters[currentIndex.value]; - console.log('test', currentIndex.value, 'start'); - console.log('test', currentIndex.value, 'start', 'chapter', chapter); chapter.onEnter?.(); for (const action of chapter.actions ?? []) { - console.log('test', currentIndex.value, 'start', 'action', action); const resolved = resolveAction(action); resolved?.add?.(); } @@ -104,7 +101,6 @@ export function useStorytelling(options: UseStorytellingOptions) { if (autoNext && chapter.duration) { clearTimer(); timer = window.setTimeout(() => { - console.log('test', currentIndex.value, 'start', 'exit'); chapter.onExit?.(); next(); }, chapter.duration / currentSpeed.value); @@ -112,28 +108,16 @@ export function useStorytelling(options: UseStorytellingOptions) { }; const exitCurrentChapter = (nextIndex: number) => { - console.log('test', currentIndex.value, 'end', 'exit'); const current = chapters[currentIndex.value]; - console.log('test', currentIndex.value, 'end', 'chapter', current); const next = chapters[nextIndex]; const nextTypes = new Set(next?.actions?.map((a) => a.type) ?? []); current?.onExit?.(); for (const action of current?.actions ?? []) { - console.log( - 'test', - currentIndex.value, - 'end', - 'action', - action, - nextTypes, - nextTypes.has(action.type) - ); if (!nextTypes.has(action.type)) { const resolved = resolveAction(action); resolved?.remove?.(); - console.log('test', currentIndex.value, 'end', 'remove action', action); } } diff --git a/apps/demo-shared/.eslintrc.json b/apps/demo-shared/.eslintrc.json index 7ed78d7..c428d22 100644 --- a/apps/demo-shared/.eslintrc.json +++ b/apps/demo-shared/.eslintrc.json @@ -4,6 +4,7 @@ "eslint:recommended", "@vue/eslint-config-typescript", "@vue/eslint-config-prettier/skip-formatting", + "../../.eslintrc.json", "../../.eslintrc.base.json" ], "ignorePatterns": ["!**/*"], @@ -13,6 +14,10 @@ "rules": { "vue/multi-word-component-names": "off" } + }, + { + "files": ["*.html"], + "processor": "vue/.vue" } ] } diff --git a/apps/demo-shared/index.html b/apps/demo-shared/index.html index f1d8be1..9ed3e98 100644 --- a/apps/demo-shared/index.html +++ b/apps/demo-shared/index.html @@ -2,7 +2,7 @@ - + demo-shared diff --git a/deploy/demo-map b/deploy/demo-map index bca4369..5735328 160000 --- a/deploy/demo-map +++ b/deploy/demo-map @@ -1 +1 @@ -Subproject commit bca4369793f986015368481f037c220bfce980d3 +Subproject commit 573532826fb79148b5eb1dc8ff93f2841d30f1cc diff --git a/libs/map/core/package.json b/libs/map/core/package.json index 8e54d16..3922574 100644 --- a/libs/map/core/package.json +++ b/libs/map/core/package.json @@ -36,7 +36,7 @@ "@turf/turf": "^6.5.0", "proj4": "^2.11.0", "lodash": "^4.17.21", - "mapbox-gl": ">=1.13.0", + "maplibre-gl": "^5.4.0", "@mapbox/mapbox-gl-sync-move": "^0.3.1" } } diff --git a/libs/map/core/src/extra/compare/modules/MapCompare.vue b/libs/map/core/src/extra/compare/modules/MapCompare.vue index e8361c2..9acae64 100644 --- a/libs/map/core/src/extra/compare/modules/MapCompare.vue +++ b/libs/map/core/src/extra/compare/modules/MapCompare.vue @@ -4,13 +4,14 @@ import { useBreakpoints } from '@hungpvq/shared-core'; import type { MapSimple } from '@hungpvq/shared-map'; import { DraggableContainer } from '@hungpvq/vue-draggable'; import syncMove from '@mapbox/mapbox-gl-sync-move'; -import mapboxgl from 'mapbox-gl'; +import { debounce } from 'lodash'; +import mapboxgl from 'maplibre-gl'; import { computed, nextTick, + onBeforeUnmount, onMounted, onUnmounted, - onBeforeUnmount, provide, ref, watch, @@ -20,7 +21,6 @@ import { actions, store as storeMap } from '../../../store/store'; import ActionControl from '../../event/modules/ActionControl.vue'; import { getMapCompareSetting, initStoreMapCompare } from '../store'; import { MapCompareSwiper, MapCompareSwiperVertical } from './helper'; -import { debounce } from 'lodash'; const breakpoints = useBreakpoints({ mobile: 0, // optional tablet: 640, @@ -41,9 +41,29 @@ const props = defineProps({ }, dragId: { type: String }, }); +function isWebglSupported() { + if (window.WebGLRenderingContext) { + const canvas = document.createElement('canvas'); + try { + // Note that { failIfMajorPerformanceCaveat: true } can be passed as a second argument + // to canvas.getContext(), causing the check to fail if hardware rendering is not available. See + // https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/getContext + // for more details. + const context = canvas.getContext('webgl2') || canvas.getContext('webgl'); + if (context && typeof context.getParameter == 'function') { + return true; + } + } catch (e) { + // WebGL is supported, but disabled + } + return false; + } + // WebGL not supported + return false; +} const emit = defineEmits(['map-loaded', 'map-destroy']); const countMap = ref(2); -const isSupport = ref(mapboxgl.supported()); +const isSupport = ref(isWebglSupported()); const loaded = ref(false); const id = ref(getUUIDv4()); onMounted(() => { @@ -203,7 +223,7 @@ function setupCompare() { } }); } -let observer; +let observer: any; const handleResize = debounce((entry) => { const { width, height } = entry.contentRect; actions.getMap(id.value, (map) => { diff --git a/libs/map/core/src/extra/event/model/Event.ts b/libs/map/core/src/extra/event/model/Event.ts index 848da61..b4f3b0e 100644 --- a/libs/map/core/src/extra/event/model/Event.ts +++ b/libs/map/core/src/extra/event/model/Event.ts @@ -1,12 +1,12 @@ import { MapSimple } from '@hungpvq/shared-map'; -import { EventData, MapLayerEventType } from 'mapbox-gl'; +import { MapLayerEventType } from 'maplibre-gl'; import { Base } from '../../../model/Base'; import { IEvent } from '../types'; export class Event< T extends keyof MapLayerEventType = 'click', IOption extends Record = any, - ICallBack = (ev: MapLayerEventType[T] & EventData) => void + ICallBack = (ev: MapLayerEventType[T]) => void > extends Base implements IEvent diff --git a/libs/map/core/src/extra/event/model/custom/EventClick.ts b/libs/map/core/src/extra/event/model/custom/EventClick.ts index adbcece..2e0c6a2 100644 --- a/libs/map/core/src/extra/event/model/custom/EventClick.ts +++ b/libs/map/core/src/extra/event/model/custom/EventClick.ts @@ -29,3 +29,29 @@ export class EventClick extends Event<'click', EventClickOption> { return this; } } + +export class EventMouseMove extends Event<'mousemove', EventClickOption> { + constructor(type_select = 'map') { + super('mousemove', type_select); + this.setClassPointer('pointer'); + } + setClassPointer(classPointer: string) { + this.options.classPointer = classPointer; + } + override addToMap(map: MapSimple) { + if (this.options.classPointer) + map.getCanvas().classList.add(this.options.classPointer); + if (this.handler) { + map.on('mousemove', this.handler); + } + return this; + } + override removeFromMap(map: MapSimple) { + if (this.options.classPointer) + map.getCanvas().classList.remove(this.options.classPointer); + if (this.handler) { + map.off('mousemove', this.handler); + } + return this; + } +} diff --git a/libs/map/core/src/extra/event/model/custom/index.ts b/libs/map/core/src/extra/event/model/custom/index.ts index 6116e03..55ab401 100644 --- a/libs/map/core/src/extra/event/model/custom/index.ts +++ b/libs/map/core/src/extra/event/model/custom/index.ts @@ -1,2 +1,2 @@ export { EventBboxRanger } from './EventBboxSelect'; -export { EventClick } from './EventClick'; +export { EventClick, EventMouseMove } from './EventClick'; diff --git a/libs/map/core/src/extra/event/types.ts b/libs/map/core/src/extra/event/types.ts index 94a3fc4..eb36587 100644 --- a/libs/map/core/src/extra/event/types.ts +++ b/libs/map/core/src/extra/event/types.ts @@ -1,5 +1,5 @@ import { Coordinates, MapSimple } from '@hungpvq/shared-map'; -import { EventData, MapLayerEventType } from 'mapbox-gl'; +import { MapLayerEventType } from 'maplibre-gl'; export interface EventClickOption { classPointer?: string; @@ -11,7 +11,7 @@ export type EventBboxRangerHandle = ( export interface IEvent< T extends keyof MapLayerEventType = 'click', IOption extends Record = any, - ICallBack = (ev: MapLayerEventType[T] & EventData) => void + ICallBack = (ev: MapLayerEventType[T]) => void > { _id: string; get id(): string; diff --git a/libs/map/core/src/extra/image/helpers.ts b/libs/map/core/src/extra/image/helpers.ts index a03723b..44e5989 100644 --- a/libs/map/core/src/extra/image/helpers.ts +++ b/libs/map/core/src/extra/image/helpers.ts @@ -18,17 +18,14 @@ function loadImageViaTag(url: string): Promise { return promise; } -export function addImageForMap( +export async function addImageForMap( map: MapSimple, key: string, url: string, option: any = {} ) { - return new Promise((resolve, reject) => { - map.loadImage(url, (error: any, image: any) => { - if (error) reject(error); - if (!map.hasImage(key)) map.addImage(key, image, option); - resolve(true); - }); - }); + const image = await map.loadImage(url); + + if (!map.hasImage(key)) map.addImage(key, image.data, option); + return true; } diff --git a/libs/map/core/src/extra/image/hooks/index.ts b/libs/map/core/src/extra/image/hooks/index.ts new file mode 100644 index 0000000..73be387 --- /dev/null +++ b/libs/map/core/src/extra/image/hooks/index.ts @@ -0,0 +1 @@ +export * from './useMapImages'; diff --git a/libs/map/core/src/extra/image/hooks/useMapImages.ts b/libs/map/core/src/extra/image/hooks/useMapImages.ts new file mode 100644 index 0000000..e2adf66 --- /dev/null +++ b/libs/map/core/src/extra/image/hooks/useMapImages.ts @@ -0,0 +1,93 @@ +import type { MapSimple } from '@hungpvq/shared-map'; +import type { StyleImage } from 'maplibre-gl'; +import { onBeforeUnmount, onMounted, shallowRef } from 'vue'; +import { getMap } from '../../../store'; +const cache: Record = {}; + +export function useMapImages(mapId: string) { + const images = shallowRef>({}); + const loadImages = (map: MapSimple) => { + if (!map) return; + const names = map.listImages(); + const result: Record = {}; + names.forEach((name: string) => { + const img = map!.getImage(name); + if (img) { + result[name] = img; + } + }); + images.value = result; + }; + let handleMap: undefined | (() => void) = undefined; + const setupListeners = (map: MapSimple) => { + if (!map) return; + handleMap = () => loadImages(map); + map.on('styledata', handleMap); + map.on('idle', handleMap); + }; + + const removeListeners = (map: MapSimple) => { + if (!map) return; + if (handleMap) { + map.off('styledata', handleMap); + map.off('idle', handleMap); + } + }; + + onMounted(() => { + getMap(mapId, (map: MapSimple) => { + loadImages(map); + setupListeners(map); + }); + }); + + onBeforeUnmount(() => { + getMap(mapId, (map: MapSimple) => { + removeListeners(map); + }); + }); + const reload = () => { + getMap(mapId, (map: MapSimple) => { + loadImages(map); + setupListeners(map); + }); + }; + function toDataURL(id: string, imageData: StyleImage): string { + if (cache[id]) { + return cache[id]; + } + + // Chuyển đổi dữ liệu của StyleImage thành ImageData + const rgbaImage = imageData.data; + const image = toImageDataFromRGBAImage(rgbaImage); + + // Tạo Canvas và vẽ dữ liệu lên đó + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d')!; + canvas.width = rgbaImage.width; + canvas.height = rgbaImage.height; + + // Vẽ ImageData vào canvas + ctx.putImageData(image, 0, 0); + + // Tạo URL từ Canvas + const imageUrl = canvas.toDataURL('image/png'); + cache[id] = imageUrl; + + return imageUrl; + } + return { + images, + reload, + toDataURL, + }; +} +function toImageDataFromRGBAImage(rgbaImage: StyleImage['data']): ImageData { + const { width, height, data } = rgbaImage; + + // Chuyển đổi từ Uint8Array sang Uint8ClampedArray + const clampedData = new Uint8ClampedArray(data); + + // Tạo ImageData từ mảng dữ liệu clamped + return new ImageData(clampedData, width, height); +} diff --git a/libs/map/core/src/extra/image/index.ts b/libs/map/core/src/extra/image/index.ts index 3eaa9dd..d6d2b99 100644 --- a/libs/map/core/src/extra/image/index.ts +++ b/libs/map/core/src/extra/image/index.ts @@ -1 +1,2 @@ +export * from './hooks'; export * as imageStore from './store'; diff --git a/libs/map/core/src/field/input-choose.vue b/libs/map/core/src/field/input-choose.vue index aa53d6e..b827dae 100644 --- a/libs/map/core/src/field/input-choose.vue +++ b/libs/map/core/src/field/input-choose.vue @@ -51,6 +51,7 @@ function onSetValue(item) { text-align: center; display: flex; align-items: center; + justify-content: center; } .item-choose-active { color: var(--v-primary-base, #1a73e8); diff --git a/libs/map/core/src/field/input-text.vue b/libs/map/core/src/field/input-text.vue index ec9ffe0..f67ce85 100644 --- a/libs/map/core/src/field/input-text.vue +++ b/libs/map/core/src/field/input-text.vue @@ -1,3 +1,8 @@ + + diff --git a/libs/map/core/src/modules/Map.vue b/libs/map/core/src/modules/Map.vue index 789bcfe..77a4c6e 100644 --- a/libs/map/core/src/modules/Map.vue +++ b/libs/map/core/src/modules/Map.vue @@ -3,7 +3,7 @@ import { getUUIDv4 } from '@hungpvq/shared'; import { useBreakpoints } from '@hungpvq/shared-core'; import type { MapSimple } from '@hungpvq/shared-map'; import { DraggableContainer } from '@hungpvq/vue-draggable'; -import mapboxgl, { MapboxOptions } from 'mapbox-gl'; +import mapboxgl, { MapOptions } from 'maplibre-gl'; import { computed, onMounted, onUnmounted, provide, ref } from 'vue'; import ActionControl from '../extra/event/modules/ActionControl.vue'; import { actions, state as mapState } from '../store/store'; @@ -16,7 +16,7 @@ const breakpoints = useBreakpoints({ laptop: 1024, desktop: 1280, }); -const DEFAULTOPTION: Partial = { +const DEFAULTOPTION: Partial = { center: [105.19084739818732, 15.827971829957548], zoom: 5.297175623863693, maxZoom: 22, @@ -41,9 +41,28 @@ const emit = defineEmits<{ // eslint-disable-next-line @typescript-eslint/no-unused-vars (_e: 'map-destroy', _map: MapSimple): void; }>(); -mapboxgl.accessToken = props.mapboxAccessToken; +function isWebglSupported() { + if (window.WebGLRenderingContext) { + const canvas = document.createElement('canvas'); + try { + // Note that { failIfMajorPerformanceCaveat: true } can be passed as a second argument + // to canvas.getContext(), causing the check to fail if hardware rendering is not available. See + // https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/getContext + // for more details. + const context = canvas.getContext('webgl2') || canvas.getContext('webgl'); + if (context && typeof context.getParameter == 'function') { + return true; + } + } catch (e) { + // WebGL is supported, but disabled + } + return false; + } + // WebGL not supported + return false; +} const mapContainer = ref(); -const isSupport = ref(mapboxgl.supported()); +const isSupport = ref(isWebglSupported()); const loaded = ref(false); let map: mapboxgl.Map | undefined = undefined; const id = ref(getUUIDv4()); @@ -52,7 +71,6 @@ const state = computed(() => mapState[id.value]); onMounted(() => { const initOptions = Object.assign({}, DEFAULTOPTION, props.initOptions); map = new mapboxgl.Map({ - accessToken: props.mapboxAccessToken, container: mapContainer.value!, style: { version: 8, @@ -170,7 +188,7 @@ const isMobile = breakpoints.smallerOrEqual('tablet'); transform: rotate(-45deg); } } -@import 'mapbox-gl/dist/mapbox-gl.css'; +@import 'maplibre-gl/dist/maplibre-gl.css'; .draggable-container * { pointer-events: all; diff --git a/libs/map/core/src/modules/index.ts b/libs/map/core/src/modules/index.ts index 5355049..75e703f 100644 --- a/libs/map/core/src/modules/index.ts +++ b/libs/map/core/src/modules/index.ts @@ -1,6 +1,7 @@ export { default as CrsControl } from './CrsControl/CrsControl.vue'; export { default as FullScreenControl } from './FullScreenControl/FullScreenControl.vue'; export { default as GeoLocateControl } from './GeoLocateControl/GeoLocateControl.vue'; +export { default as GlobeControl } from './GlobeControl/GlobeControl.vue'; export { default as GotoControl } from './GotoControl/GotoControl.vue'; export { default as HomeControl } from './HomeControl/HomeControl.vue'; export { default as Map } from './Map.vue'; diff --git a/libs/map/core/src/utils/fillBound.ts b/libs/map/core/src/utils/fillBound.ts index 9779cc5..e49c6a6 100644 --- a/libs/map/core/src/utils/fillBound.ts +++ b/libs/map/core/src/utils/fillBound.ts @@ -1,4 +1,4 @@ -import { EaseToOptions, PaddingOptions } from 'mapbox-gl'; +import { EaseToOptions, PaddingOptions } from 'maplibre-gl'; import { CoordinatesNumber, MapSimple } from '@hungpvq/shared-map'; import { bbox, lineString, point, polygon } from '@turf/turf'; diff --git a/libs/map/dataset/.eslintrc.json b/libs/map/dataset/.eslintrc.json index 04e3c00..0e341fb 100644 --- a/libs/map/dataset/.eslintrc.json +++ b/libs/map/dataset/.eslintrc.json @@ -8,6 +8,19 @@ ], "ignorePatterns": ["!**/*"], "overrides": [ + { + "files": ["*.ts", "*.tsx"], + "parser": "@typescript-eslint/parser", + "rules": { + "@typescript-eslint/consistent-type-imports": [ + "error", + { + "prefer": "type-imports", + "disallowTypeAnnotations": false + } + ] + } + }, { "files": ["*.ts", "*.tsx", "*.js", "*.jsx", "*.vue"], "rules": { diff --git a/libs/map/dataset/package.json b/libs/map/dataset/package.json index b138136..43a243a 100644 --- a/libs/map/dataset/package.json +++ b/libs/map/dataset/package.json @@ -29,7 +29,7 @@ "@hungpvq/vue-map-core": ">=0.0.1", "@hungpvq/content-menu": ">=0.0.1", "@turf/turf": "^6.5.0", - "mapbox-gl": ">=1.13.0", + "maplibre-gl": "^5.4.0", "vuedraggable": "^4.1.0", "@hungpvq/shared-file": ">=0.0.1", "lodash": "^4.17.21" diff --git a/libs/map/dataset/src/builder/geojson.ts b/libs/map/dataset/src/builder/geojson.ts index e40eec9..e4d3ee0 100644 --- a/libs/map/dataset/src/builder/geojson.ts +++ b/libs/map/dataset/src/builder/geojson.ts @@ -1,7 +1,8 @@ import type { Color } from '@hungpvq/shared-map'; import { getChartRandomColor } from '@hungpvq/vue-map-core'; import type { GeoJSON } from 'geojson'; -import { IDataset, IListViewUI } from '../interfaces'; +import type { IDataset, IListViewUI } from '../interfaces'; +import type { DatasetComposite } from '../model'; import { createDataManagementMapboxComponent, createDataset, @@ -11,7 +12,6 @@ import { createMenuItemShowDetailForItem, createMenuItemToBoundActionForItem, createMultiMapboxLayerComponent, - DatasetComposite, } from '../model'; import { LayerSimpleMapboxBuild } from '../utils/layer-simple-builder'; export type GeojsonDatasetOption = { diff --git a/libs/map/dataset/src/builder/raster.ts b/libs/map/dataset/src/builder/raster.ts index a986d94..b82e15d 100644 --- a/libs/map/dataset/src/builder/raster.ts +++ b/libs/map/dataset/src/builder/raster.ts @@ -1,5 +1,5 @@ -import type { BBox } from 'geojson'; -import { IDataset } from '../interfaces'; +import type { RasterSourceSpecification } from 'maplibre-gl'; +import type { IDataset } from '../interfaces'; import { createDataset, createDatasetPartListViewUiComponent, @@ -11,7 +11,7 @@ import { export type RasterUrlDatasetOption = { name: string; tiles: string[]; - bounds?: BBox; + bounds?: RasterSourceSpecification['bounds']; maxZoom?: number; minZoom?: number; }; @@ -23,7 +23,6 @@ export function createRasterUrlDataset(data: RasterUrlDatasetOption): IDataset { ) as DatasetComposite; const source_raster = createDatasetPartRasterSourceComponent(data.name, { - name: data.name, type: 'raster', tiles: data.tiles, maxzoom: data.maxZoom, diff --git a/libs/map/dataset/src/interfaces/dataset.parts.ts b/libs/map/dataset/src/interfaces/dataset.parts.ts index de67050..352d72b 100644 --- a/libs/map/dataset/src/interfaces/dataset.parts.ts +++ b/libs/map/dataset/src/interfaces/dataset.parts.ts @@ -1,7 +1,7 @@ import type { Color, MapSimple } from '@hungpvq/shared-map'; -import { IDrawHandler } from '@hungpvq/vue-map-core'; +import type { IDrawHandler } from '@hungpvq/vue-map-core'; import type { BBox } from 'geojson'; -import type { AnySourceData, PointLike } from 'mapbox-gl'; +import type { PointLike, SourceSpecification } from 'maplibre-gl'; import type { IDataset } from './dataset.base'; import type { IDatasetMap } from './dataset.map'; @@ -86,10 +86,15 @@ export type IListViewUI = T & }; export type IMapboxSourceView = IDatasetMap & { - getMapboxSource: () => AnySourceData; + getMapboxSource: () => SourceSpecification & { id?: string }; updateData?(map: MapSimple, data: any): void; getFieldsInfo(): IFieldInfo[]; getDataInfo(): any; + getSourceId(): string; + hightLight?( + map: MapSimple, + geojsonData: GeoJSON.Feature | undefined + ): void; }; export type IMapboxLayerView = IDatasetMap & { diff --git a/libs/map/dataset/src/model/data-management/base.ts b/libs/map/dataset/src/model/data-management/base.ts index 3dc70a1..16d9cfd 100644 --- a/libs/map/dataset/src/model/data-management/base.ts +++ b/libs/map/dataset/src/model/data-management/base.ts @@ -1,4 +1,4 @@ -import { IDataManagementView, IDataset } from '../../interfaces'; +import type { IDataManagementView, IDataset } from '../../interfaces'; import { createNamedComponent } from '../base'; import { createDatasetLeaf } from '../dataset.base.function'; diff --git a/libs/map/dataset/src/model/data-management/model.ts b/libs/map/dataset/src/model/data-management/model.ts index 0b80586..fa52f58 100644 --- a/libs/map/dataset/src/model/data-management/model.ts +++ b/libs/map/dataset/src/model/data-management/model.ts @@ -6,6 +6,10 @@ import type { Feature } from 'geojson'; import LayerDetail from '../../modules/LayerDetail/LayerDetail.vue'; import { addComponent, setFeatureHighlight } from '../../store'; import { isDatasetSourceMap } from '../../utils/check'; +import { + convertFeatureToItem, + convertItemToFeature, +} from '../../utils/convert'; import { createNamedComponent } from '../base'; import { findSiblingOrNearestLeaf } from '../dataset.visitors'; import { createDatasetPartDataManagementComponent } from './base'; @@ -53,8 +57,14 @@ export function createDataManagementMapboxComponent< fields: config.fields, view: dataComponent, }, + check: 'detail', }); - setFeatureHighlight(mapId, convertItemToFeature(detail), 'detail'); + setFeatureHighlight( + mapId, + convertItemToFeature(detail), + 'detail', + dataComponent + ); }; const dataComponent = createNamedComponent('DataManagementMapboxComponent', { @@ -101,21 +111,3 @@ export function createDataManagementMapboxComponent< return dataComponent; } - -function convertFeatureToItem(feature: Feature): T { - return { - id: feature.id, - ...feature.properties, - geometry: feature.geometry, - } as T; -} - -function convertItemToFeature(item: any): Feature { - const { geometry, ...properties } = item; - return { - id: item.id, - type: 'Feature', - geometry, - properties, - }; -} diff --git a/libs/map/dataset/src/model/dataset.base.function.ts b/libs/map/dataset/src/model/dataset.base.function.ts index 03b4e7d..9ed31ac 100644 --- a/libs/map/dataset/src/model/dataset.base.function.ts +++ b/libs/map/dataset/src/model/dataset.base.function.ts @@ -1,4 +1,4 @@ -import { IDataset, IDatasetVisitor } from '../interfaces'; +import type { IDataset, IDatasetVisitor } from '../interfaces'; import { createBase } from './base'; export function createDatasetComponent( diff --git a/libs/map/dataset/src/model/identify/identifyMapboxMerged.ts b/libs/map/dataset/src/model/identify/identifyMapboxMerged.ts index 7c604a4..3e94efc 100644 --- a/libs/map/dataset/src/model/identify/identifyMapboxMerged.ts +++ b/libs/map/dataset/src/model/identify/identifyMapboxMerged.ts @@ -1,7 +1,6 @@ import { getMap } from '@hungpvq/vue-map-core'; -import { MapboxGeoJSONFeature, PointLike } from 'mapbox-gl'; -import { - IDataManagementView, +import type { MapGeoJSONFeature, PointLike } from 'maplibre-gl'; +import type { IDataset, IdentifyResult, IIdentifyViewWithMerge, @@ -18,13 +17,13 @@ function removeDuplicates( collected: { identify: IIdentifyViewWithMerge; identifyId: string; - feature: MapboxGeoJSONFeature; + feature: MapGeoJSONFeature; rawId: string; }[] ): { identify: IIdentifyViewWithMerge; identifyId: string; - feature: MapboxGeoJSONFeature; + feature: MapGeoJSONFeature; rawId: string; }[] { const seen = new Set(); @@ -41,7 +40,7 @@ function formatFeature( collected: { identify: IIdentifyViewWithMerge; identifyId: string; - feature: MapboxGeoJSONFeature; + feature: MapGeoJSONFeature; rawId: string; }[], dataMap: Map @@ -96,7 +95,7 @@ export async function getMergedFeatures( getMap(payload.mapId, (map) => { const allLayerIds = Object.keys(layerIdMap); - const queriedFeatures: MapboxGeoJSONFeature[] = map.queryRenderedFeatures( + const queriedFeatures: MapGeoJSONFeature[] = map.queryRenderedFeatures( payload.pointOrBox, { layers: allLayerIds } ); @@ -104,7 +103,7 @@ export async function getMergedFeatures( const collected: { identify: IIdentifyViewWithMerge; identifyId: string; - feature: MapboxGeoJSONFeature; + feature: MapGeoJSONFeature; rawId: string; }[] = []; diff --git a/libs/map/dataset/src/model/identify/models.ts b/libs/map/dataset/src/model/identify/models.ts index a8fa710..1e696b0 100644 --- a/libs/map/dataset/src/model/identify/models.ts +++ b/libs/map/dataset/src/model/identify/models.ts @@ -1,6 +1,6 @@ import type { MapSimple } from '@hungpvq/shared-map'; import { getMap } from '@hungpvq/vue-map-core'; -import type { MapboxGeoJSONFeature, PointLike } from 'mapbox-gl'; +import type { MapGeoJSONFeature, PointLike } from 'maplibre-gl'; import type { IDataset } from '../../interfaces/dataset.base'; import type { IDataManagementView, @@ -10,6 +10,7 @@ import type { IMapboxLayerView, } from '../../interfaces/dataset.parts'; import { isIdentifyMergeView, isMapboxLayerView } from '../../utils/check'; +import { convertFeatureToItem } from '../../utils/convert'; import { createNamedComponent } from '../base'; import { createDatasetLeaf } from '../dataset.base.function'; import { @@ -67,7 +68,7 @@ export function createIdentifyMapboxComponent(name: string, config?: any) { const allLayerIds: string[] = Array.from(results.values()).flat(2); getMap(mapId, (map: MapSimple) => { - const features: MapboxGeoJSONFeature[] = map.queryRenderedFeatures( + const features: MapGeoJSONFeature[] = map.queryRenderedFeatures( pointOrBox, { layers: allLayerIds, @@ -194,3 +195,58 @@ export async function handleMultiIdentify( return Promise.all(promises).then((res) => res.flat()); } + +export async function handleMultiIdentifyGetFirst( + identifies: IIdentifyView[], + mapId: string, + pointOrBox?: PointLike | [PointLike, PointLike] +): Promise { + const allLayerIds: string[] = []; + const cache: Record = {}; + identifies.forEach((identify) => { + const results = runAllComponentsWithCheck( + identify.getParent() || identify, + (dataset): dataset is IDataset & IMapboxLayerView => + isMapboxLayerView(dataset), + [ + (dataset) => { + return dataset.getAllLayerIds(); + }, + ] + ); + const layerIds = Array.from(results.values()).flat(2); + layerIds.forEach((layerId) => { + cache[layerId] = identify; + }); + allLayerIds.push(...layerIds); + }); + return new Promise((resolve) => { + getMap(mapId, (map: MapSimple) => { + const features: MapGeoJSONFeature[] = map.queryRenderedFeatures( + pointOrBox, + { + layers: allLayerIds, + } + ); + if (features.length > 0) { + const x = features[0]; + const datasetPartIdentify = cache[x.layer.id]; + const id = + x.properties?.[datasetPartIdentify?.config?.field_id || 'id'] ?? x.id; + const name = + x.properties?.[datasetPartIdentify?.config?.field_name || 'id'] ?? + x.id; + resolve({ + identify: datasetPartIdentify, + features: [ + { + id, + name, + data: convertFeatureToItem(x), + }, + ], + }); + } + }); + }); +} diff --git a/libs/map/dataset/src/model/layer/base.ts b/libs/map/dataset/src/model/layer/base.ts index 8fe6ef6..1824b05 100644 --- a/libs/map/dataset/src/model/layer/base.ts +++ b/libs/map/dataset/src/model/layer/base.ts @@ -1,5 +1,5 @@ import type { MapSimple } from '@hungpvq/shared-map'; -import { IDataset, IMapboxLayerView } from '../../interfaces'; +import type { IDataset, IMapboxLayerView } from '../../interfaces'; import { createNamedComponent } from '../base'; import { createDatasetLeaf } from '../dataset.base.function'; import { createDatasetMenu } from '../part-menu.model'; diff --git a/libs/map/dataset/src/model/layer/model.ts b/libs/map/dataset/src/model/layer/model.ts index 67f3dff..a31d417 100644 --- a/libs/map/dataset/src/model/layer/model.ts +++ b/libs/map/dataset/src/model/layer/model.ts @@ -1,31 +1,39 @@ -import { copyByJson } from '@hungpvq/shared'; +import { copyByJson, getUUIDv4 } from '@hungpvq/shared'; import type { MapSimple } from '@hungpvq/shared-map'; -import type { AnyLayer, Layer } from 'mapbox-gl'; +import type { LayerSpecification } from 'maplibre-gl'; import MultiStyle from '../../modules/StyleControl/style/multi-style.vue'; import { createNamedComponent } from '../base'; import { findFirstLeafByType } from '../dataset.visitors'; import { createDatasetPartMapboxLayerComponent } from './base'; - +type BaseLayerSpec = Partial> & { id?: string }; export function createMultiMapboxLayerComponent( name: string, - data: Partial[] = [] + data: BaseLayerSpec[] = [] ) { - const base = createDatasetPartMapboxLayerComponent[]>( + const base = createDatasetPartMapboxLayerComponent( name, data ); const cacheOpacity: Record = {}; - // Gán id nếu chưa có - base.getData().forEach((layer, index: number) => { - if (!layer.id) { - layer.id = `${base.id}-${index}`; - } - cacheOpacity[layer.id] = layer.paint?.[getKeyOpacity(layer)] ?? 1; + base.getData().forEach((layer) => { + const layer_id = layer.id || getUUIDv4(); + layer.id = layer_id; + cacheOpacity[layer_id] = layer.paint?.[getKeyOpacity(layer)] ?? 1; }); return createNamedComponent('MultiMapboxLayerComponent', { ...base, + setData(newData: BaseLayerSpec[]) { + base.setData( + newData.map((layer) => { + const layer_id = layer.id || getUUIDv4(); + layer.id = layer_id; + cacheOpacity[layer_id] = layer.paint?.[getKeyOpacity(layer)] ?? 1; + return layer; + }) + ); + }, getBeforeId(): string | undefined { return base.getData()[0]?.id; }, @@ -42,10 +50,10 @@ export function createMultiMapboxLayerComponent( const source = findFirstLeafByType(base, 'source'); base.getData().forEach((layer) => { if (!map.getLayer(layer.id!)) { - if (!layer.source && source?.id) { - layer.source = source.id; + if (!(layer as any).source && source) { + (layer as any).source = (source as any).getSourceId(); } - map.addLayer(layer as AnyLayer, beforeId); + map.addLayer(layer as LayerSpecification, beforeId); } }); }, @@ -96,6 +104,7 @@ export function createMultiMapboxLayerComponent( ) { const { type, index } = value; let { layer } = value; + const source = findFirstLeafByType(base, 'source'); switch (type) { case 'update-one-layer': @@ -110,7 +119,7 @@ export function createMultiMapboxLayerComponent( layer = { ...layer, id: `${base.id}-${base.getData().length}`, - source: base.getData()[0]?.source, + source: (source as any).getSourceId(), }; map.addLayer(layer); base.getData().push(layer); @@ -147,8 +156,10 @@ function updateStyleLayer(map: MapSimple, old: any, newVal: any) { } } -function getKeyOpacity(layer: Partial): keyof Layer['paint'] { +function getKeyOpacity( + layer: BaseLayerSpec +): keyof LayerSpecification['paint'] { const keyOpacity = layer.type === 'symbol' ? 'icon-opacity' : `${layer.type}-opacity`; - return keyOpacity as keyof Layer['paint']; + return keyOpacity as keyof LayerSpecification['paint']; } diff --git a/libs/map/dataset/src/model/part-menu.model.ts b/libs/map/dataset/src/model/part-menu.model.ts index 8818e72..057fe9a 100644 --- a/libs/map/dataset/src/model/part-menu.model.ts +++ b/libs/map/dataset/src/model/part-menu.model.ts @@ -1,7 +1,7 @@ import { fitBounds } from '@hungpvq/shared-map'; import { getMap } from '@hungpvq/vue-map-core'; import { mdiCrosshairsGps, mdiFormatLineStyle, mdiInformation } from '@mdi/js'; -import { +import type { IActionForView, IDataManagementView, IDataset, @@ -103,7 +103,8 @@ export function createMenuItemToBoundActionForItem() { geometry, properties, }, - 'identify' + 'identify', + layer ); }); }, @@ -143,6 +144,7 @@ export function createMenuItemShowDetailInfoSource() { fields: source.getFieldsInfo(), view: layer, }, + check: 'detail', }); }, }); @@ -163,11 +165,14 @@ export function createMenuItemStyleEdit() { }); } -export function createMenuItemToggleShow() { +export function createMenuItemToggleShow( + menu: Partial> +) { return createMenuItem({ type: 'item', location: 'bottom', name: 'ToggleShow', component: () => ToggleShow, + ...menu, }); } diff --git a/libs/map/dataset/src/model/source/base.ts b/libs/map/dataset/src/model/source/base.ts index 49fe02b..ba9bb2d 100644 --- a/libs/map/dataset/src/model/source/base.ts +++ b/libs/map/dataset/src/model/source/base.ts @@ -1,6 +1,6 @@ import type { MapSimple } from '@hungpvq/shared-map'; -import type { AnySourceData } from 'mapbox-gl'; -import { IDataset, IDatasetMap, IMapboxSourceView } from '../../interfaces'; +import type { SourceSpecification } from 'maplibre-gl'; +import type { IDataset, IMapboxSourceView } from '../../interfaces'; import { createNamedComponent } from '../base'; import { createDatasetLeaf } from '../dataset.base.function'; @@ -15,17 +15,27 @@ export function createDatasetPartMapboxSourceComponent( get type() { return 'source'; }, + getSourceId() { + const source = this.getMapboxSource(); + const source_id = source.id || base.id; + return source_id; + }, addToMap(map: MapSimple) { - if (base.id && !map.getSource(base.id)) { - map.addSource(base.id, this.getMapboxSource()); + const source_id = this.getSourceId(); + if (source_id && !map.getSource(source_id)) { + map.addSource(source_id, this.getMapboxSource()); } }, removeFromMap(map: MapSimple) { - if (base.id && map.getSource(base.id)) { - map.removeSource(base.id); + const source_id = this.getSourceId(); + if (source_id && map.getLayer(source_id + '-hightLight')) { + map.removeLayer(source_id + '-hightLight'); + } + if (source_id && map.getSource(source_id)) { + map.removeSource(source_id); } }, - getMapboxSource(): AnySourceData { + getMapboxSource(): SourceSpecification & { id?: string } { throw new Error('Method not implemented.'); }, getFieldsInfo(): any[] { diff --git a/libs/map/dataset/src/model/source/model.ts b/libs/map/dataset/src/model/source/model.ts index 55abd77..c5bc7c3 100644 --- a/libs/map/dataset/src/model/source/model.ts +++ b/libs/map/dataset/src/model/source/model.ts @@ -1,13 +1,21 @@ import type { MapSimple } from '@hungpvq/shared-map'; -import { GeoJSONSource, GeoJSONSourceRaw, RasterSource } from 'mapbox-gl'; -import { IDataset, IMapboxSourceView, IMetadataView } from '../../interfaces'; +import type { + GeoJSONSource, + GeoJSONSourceSpecification, + RasterSourceSpecification, +} from 'maplibre-gl'; +import type { + IDataset, + IMapboxSourceView, + IMetadataView, +} from '../../interfaces'; import { createNamedComponent } from '../base'; import { findSiblingOrNearestLeaf } from '../dataset.visitors'; import { createDatasetPartMapboxSourceComponent } from './base'; export function createDatasetPartGeojsonSourceComponent( name: string, - data?: GeoJSONSourceRaw['data'] + data?: GeoJSONSourceSpecification['data'] ): IMapboxSourceView & IDataset { const base = createDatasetPartMapboxSourceComponent(name, data); @@ -54,11 +62,34 @@ export function createDatasetPartGeojsonSourceComponent( } base.setData(data); }, + hightLight(map: MapSimple, geojsonData: GeoJSON.Feature) { + const layer = map.getLayer(base.id + '-hightLight'); + if (!layer) { + map.addLayer({ + id: base.id + '-hightLight', + source: base.id, + type: 'line', + filter: ['==', ['get', 'id'], geojsonData?.properties?.id || null], + paint: { + 'line-color': '#004E98', + 'line-width': 4, + 'line-dasharray': [2, 2], + }, + }); + } else { + map.setFilter(base.id + '-hightLight', [ + '==', + ['get', 'id'], + geojsonData?.properties?.id || null, + ]); + } + map.moveLayer(base.id + '-hightLight'); + }, }); } export function createDatasetPartRasterSourceComponent( name: string, - data?: RasterSource + data?: RasterSourceSpecification ): IMapboxSourceView & IDataset { const base = createDatasetPartMapboxSourceComponent(name, data); diff --git a/libs/map/dataset/src/modules/CreateControl/helper/custom/ConfigRasterJsonHelper.ts b/libs/map/dataset/src/modules/CreateControl/helper/custom/ConfigRasterJsonHelper.ts index ad66579..ce70a03 100644 --- a/libs/map/dataset/src/modules/CreateControl/helper/custom/ConfigRasterJsonHelper.ts +++ b/libs/map/dataset/src/modules/CreateControl/helper/custom/ConfigRasterJsonHelper.ts @@ -1,7 +1,5 @@ -import { - createRasterUrlDataset, - RasterUrlDatasetOption, -} from '../../../../builder'; +import type { RasterUrlDatasetOption } from '../../../../builder'; +import { createRasterUrlDataset } from '../../../../builder'; import { ConfigRasterJson } from '../../config'; import { ConfigHelper } from '../_default'; diff --git a/libs/map/dataset/src/modules/CreateControl/helper/custom/ConfigRasterUrlHelper.ts b/libs/map/dataset/src/modules/CreateControl/helper/custom/ConfigRasterUrlHelper.ts index 4df8ad6..72554d0 100644 --- a/libs/map/dataset/src/modules/CreateControl/helper/custom/ConfigRasterUrlHelper.ts +++ b/libs/map/dataset/src/modules/CreateControl/helper/custom/ConfigRasterUrlHelper.ts @@ -1,7 +1,5 @@ -import { - createRasterUrlDataset, - RasterUrlDatasetOption, -} from '../../../../builder'; +import type { RasterUrlDatasetOption } from '../../../../builder'; +import { createRasterUrlDataset } from '../../../../builder'; import { ConfigRasterUrl } from '../../config'; import { ConfigHelper } from '../_default'; diff --git a/libs/map/dataset/src/modules/CreateControl/helper/custom/Geojsonhelper.ts b/libs/map/dataset/src/modules/CreateControl/helper/custom/Geojsonhelper.ts index 17ba2c6..d65da98 100644 --- a/libs/map/dataset/src/modules/CreateControl/helper/custom/Geojsonhelper.ts +++ b/libs/map/dataset/src/modules/CreateControl/helper/custom/Geojsonhelper.ts @@ -1,7 +1,5 @@ -import { - createGeoJsonDataset, - GeojsonDatasetOption, -} from '../../../../builder'; +import type { GeojsonDatasetOption } from '../../../../builder'; +import { createGeoJsonDataset } from '../../../../builder'; import { GeojsonUpload } from '../../config'; import { ConfigHelper } from '../_default'; diff --git a/libs/map/dataset/src/modules/IdentifyControl/IdentifyControl.vue b/libs/map/dataset/src/modules/IdentifyControl/IdentifyControl.vue index af2b1a0..d47d149 100644 --- a/libs/map/dataset/src/modules/IdentifyControl/IdentifyControl.vue +++ b/libs/map/dataset/src/modules/IdentifyControl/IdentifyControl.vue @@ -14,7 +14,7 @@ import { } from '@hungpvq/vue-map-core'; import SvgIcon from '@jamescoyle/vue-icon'; import { mdiCursorPointer, mdiHandPointingUp, mdiSelect } from '@mdi/js'; -import { MapMouseEvent, type PointLike } from 'mapbox-gl'; +import { MapMouseEvent, type PointLike } from 'maplibre-gl'; import { computed, onMounted, reactive, ref, watch } from 'vue'; import type { IDataset } from '../../interfaces/dataset.base'; import type { IIdentifyView, MenuAction } from '../../interfaces/dataset.parts'; @@ -29,7 +29,7 @@ const path = { }; const props = defineProps({ ...withMapProps, - immediately: { type: Boolean, default: true }, + immediately: Boolean, }); const { mapId, moduleContainerProps } = useMap(props); const { trans, setLocale } = useLang(mapId.value); @@ -205,10 +205,10 @@ function onMenuAction( menu: MenuAction, item: any ) { - if (menu.type != 'item' || !identify || !menu.click) { + if (menu.type != 'item' || !identify) { return; } - menu.click(identify, mapId.value, item); + if ('click' in menu) menu.click(identify, mapId.value, item); } onMounted(() => { if (props.immediately) onUseMapClick(); diff --git a/libs/map/dataset/src/modules/IdentifyControl/IdentifyShowFirstControl.vue b/libs/map/dataset/src/modules/IdentifyControl/IdentifyShowFirstControl.vue new file mode 100644 index 0000000..ba177ee --- /dev/null +++ b/libs/map/dataset/src/modules/IdentifyControl/IdentifyShowFirstControl.vue @@ -0,0 +1,98 @@ + + diff --git a/libs/map/dataset/src/modules/LayerHighlight/LayerHighlight.vue b/libs/map/dataset/src/modules/LayerHighlight/LayerHighlight.vue index 822980d..beb63dc 100644 --- a/libs/map/dataset/src/modules/LayerHighlight/LayerHighlight.vue +++ b/libs/map/dataset/src/modules/LayerHighlight/LayerHighlight.vue @@ -6,10 +6,15 @@ import type { MapSimple } from '@hungpvq/shared-map'; import { useMap, withMapProps } from '@hungpvq/vue-map-core'; import type { Feature, FeatureCollection } from '@turf/turf'; import { center } from '@turf/turf'; -import type { FillLayer, GeoJSONSourceRaw } from 'mapbox-gl'; -import { Marker } from 'mapbox-gl'; +import type { FillLayer, GeoJSONSourceRaw } from 'maplibre-gl'; +import { Marker } from 'maplibre-gl'; import { computed, watch } from 'vue'; -import { getFeatureHighlight } from '../../store/highlight'; +import { IMapboxSourceView } from '../../interfaces'; +import { findSiblingOrNearestLeaf } from '../../model/dataset.visitors'; +import { + getDatesetHighlight, + getFeatureHighlight, +} from '../../store/highlight'; const props = defineProps({ ...withMapProps, @@ -32,28 +37,28 @@ let marker: Marker | undefined = undefined; const { mapId, callMap } = useMap(props); const storeFeature = computed(() => { - return ( - getFeatureHighlight(mapId.value)?.value || { - type: 'FeatureCollection' as const, - features: [], - } - ); + return getFeatureHighlight(mapId.value)?.value; }); const updateSource = ( map: mapboxgl.Map, - geojsonData: - | GeoJSON.Feature - | GeoJSON.FeatureCollection - | string + geojsonData?: GeoJSON.Feature ) => { const source = map.getSource(sourceId) as mapboxgl.GeoJSONSource; if (source) { - source.setData(geojsonData); + source.setData( + geojsonData || { + type: 'FeatureCollection' as const, + features: [], + } + ); } else { map.addSource(sourceId, { type: 'geojson', - data: geojsonData, + data: geojsonData || { + type: 'FeatureCollection' as const, + features: [], + }, }); } }; @@ -98,12 +103,20 @@ const updateMarker = ( } }; -function updateHighlight( - geojsonData: - | GeoJSON.Feature - | GeoJSON.FeatureCollection - | string -) { +function updateHighlight(geojsonData?: GeoJSON.Feature) { + const dataset = getDatesetHighlight(mapId.value); + if (dataset) { + const source = findSiblingOrNearestLeaf( + dataset, + (dataset) => dataset.type == 'source' + ) as unknown as IMapboxSourceView; + if (source && 'hightLight' in source) { + callMap((map: MapSimple) => { + source.hightLight?.(map, geojsonData); + }); + return; + } + } callMap((map: MapSimple) => { updateSource(map, geojsonData); updateLayer(map); diff --git a/libs/map/dataset/src/modules/StyleControl/component/tab-content.vue b/libs/map/dataset/src/modules/StyleControl/component/tab-content.vue index 2c292cb..fc6497b 100644 --- a/libs/map/dataset/src/modules/StyleControl/component/tab-content.vue +++ b/libs/map/dataset/src/modules/StyleControl/component/tab-content.vue @@ -2,7 +2,7 @@

{{ value || default_value }} @@ -15,6 +15,7 @@ :modelValue="form" @update:modelValue="form = $event" :mapId="mapId" + class="tab-item-content" >


@@ -29,6 +30,7 @@
diff --git a/libs/map/dataset/src/modules/StyleControl/component/tab-item.vue b/libs/map/dataset/src/modules/StyleControl/component/tab-item.vue index 119262f..1eea438 100644 --- a/libs/map/dataset/src/modules/StyleControl/component/tab-item.vue +++ b/libs/map/dataset/src/modules/StyleControl/component/tab-item.vue @@ -1,17 +1,17 @@ diff --git a/libs/map/dataset/src/modules/StyleControl/lang/style/circle-style.json b/libs/map/dataset/src/modules/StyleControl/lang/style/circle-style.json index 2073cd1..ddade3b 100644 --- a/libs/map/dataset/src/modules/StyleControl/lang/style/circle-style.json +++ b/libs/map/dataset/src/modules/StyleControl/lang/style/circle-style.json @@ -8,6 +8,7 @@ "circle-pitch-alignment": "Pitch alignment", "circle-pitch-scale": "Pitch scale", "stroke-width": "Stroke width", - "stroke-opacity": "Stroke opacity" + "stroke-opacity": "Stroke opacity", + "translate": "Translate" } } diff --git a/libs/map/dataset/src/modules/StyleControl/lang/style/symbol-style.json b/libs/map/dataset/src/modules/StyleControl/lang/style/symbol-style.json index 6f0506a..10a9959 100644 --- a/libs/map/dataset/src/modules/StyleControl/lang/style/symbol-style.json +++ b/libs/map/dataset/src/modules/StyleControl/lang/style/symbol-style.json @@ -6,20 +6,50 @@ "placement": "Placement" }, "setting": { - "text-field": "Text field", + "text-font": "Font", "text-color": "Color", - "text-size": "Size", - "text-opacity": "Opacity", - "text-transform": "Transform", - "text-letter-spacing": "Letter spacing", - "text-line-height": "Line height", - "text-max-width": "Max width", - "text-halo-blur": "Halo blur", - "text-halo-width": "Halo width", - "text-halo-color": "Halo color", - "icon-image": "Image", - "icon-size": "Size", - "icon-opacity": "Opacity", - "icon-text-fit": "Fill icon to text" + "text-field": "Content", + "image": "Image", + "color": "Color", + "opacity": "Opacity", + "font": "Font", + "letter-spacing": "Letter spacing", + "line-height": "Line height", + "max-width": "Max width", + "transform": "Transform", + "halo-color": "Halo color", + "halo-width": "Halo width", + "halo-blur": "Halo blur", + "fit-icon": "Fit icon to text", + "size": "Size", + "icon-opacity": "Icon opacity", + "icon-size": "Icon size", + "icon-text-fit": "Fit icon to text", + "icon-text-fit-padding": "Icon-text padding", + "icon-image": "Icon image", + "text-justify": "Text justification", + "text-anchor": "Text anchor", + "text-offset": "Text offset", + "text-radial-offset": "Text radial offset", + "text-translate": "Text translation", + "text-rotate": "Text rotation", + "text-pitch-alignment": "Text pitch alignment", + "icon-offset": "Icon offset", + "icon-translate": "Icon translation", + "icon-rotate": "Icon rotation", + "symbol-placement": "Symbol placement", + "symbol-spacing": "Symbol spacing", + "text-max-angle": "Max text angle", + "avoid-edges": "Avoid edges", + "sort-key": "Sort key", + "text-orientation": "Text orientation", + "text-padding": "Text padding", + "allow-text-overlap": "Allow text overlap", + "text-ignore-placement": "Text ignore placement", + "text-optional": "Text optional", + "icon-padding": "Icon padding", + "allow-icon-overlap": "Allow icon overlap", + "icon-ignore-placement": "Icon ignore placement", + "icon-optional": "Icon optional" } } diff --git a/libs/map/dataset/src/modules/StyleControl/style-control.vue b/libs/map/dataset/src/modules/StyleControl/style-control.vue index 6292fe3..5ab5c07 100644 --- a/libs/map/dataset/src/modules/StyleControl/style-control.vue +++ b/libs/map/dataset/src/modules/StyleControl/style-control.vue @@ -39,16 +39,7 @@ let layer_map_component: any = shallowRef(''); onMounted(() => { toggleShow(true); layer_map.value = undefined; - const layerView = findSiblingOrNearestLeaf(props.item, (dataset) => - isMapboxLayerView(dataset) - ); - if (layerView) { - if (isMapboxLayerView(layerView)) { - layer_map.value = layerView || undefined; - layer_map_component.value = layerView.getComponentUpdate(); - layer.value = copyByJson(layerView.getData()); - } - } + updateValue(); }); const onClose = () => { layer_map.value = undefined; @@ -62,6 +53,19 @@ const onUpdateStyle = (value: any) => { } layer_map.value.updateValue(map, value); }); + updateValue(); +}; +const updateValue = () => { + const layerView = findSiblingOrNearestLeaf(props.item, (dataset) => + isMapboxLayerView(dataset) + ); + if (layerView) { + if (isMapboxLayerView(layerView)) { + layer_map.value = layerView || undefined; + layer_map_component.value = layerView.getComponentUpdate(); + layer.value = copyByJson(layerView.getData()); + } + } };