import L, { icon, Evented as LEvented, type LatLng, Map as LMap, geoJSON as LeafletGeoJSON, FeatureGroup, } from 'leaflet' import { currentMapBBoxRef, currentZoomRef, durationRef, numberOfPolygonsRef, numberOfPredictedMasksRef, layerControlGroupLayersRef, mapNavigationLocked, OpenStreetMap, responseMessageRef, responseMessageLisaRef, waitingString } from './constants' import { ExcludeIncludeLabelPrompt as excludeIncludeLabelPrompt, type ArrayNumber, type BboxLatLng, type ExcludeIncludeLabelPrompt, type IBodyLatLngPoints, type IPointPrompt, type IRectanglePrompt, type IRectangleTable, type IPointTable, type SourceTileType, type ServiceTiles, type IBodyLatLngWithStringPoints } from './types.d' import { type Ref } from 'vue' export const updateZoomBboxMap = (localMap: LMap) => { currentZoomRef.value = localMap.getZoom() currentMapBBoxRef.value = getExtentCurrentViewMapBBox(localMap) } export const getCurrentBasemap = (url: string, providersArray: ServiceTiles): string => { for (const [key, value] of Object.entries(providersArray)) { if (value._url == url) { return key } } return "-" } const logErrorOnRequest = (fnName: string, errorsArgs: Object): void => { for (const [key, value] of Object.entries(errorsArgs)) { console.error(`error:${fnName}:: ${key}: `, value) } } export const sendMLStringRequest = async (leafletMap: LMap, promptRequest: string, sourceType: SourceTileType = OpenStreetMap) => { console.log("sendMLStringRequest:: start") mapNavigationLocked.value = true const bodyRequest: IBodyLatLngWithStringPoints = { bbox: getExtentCurrentViewMapBBox(leafletMap), string_prompt: promptRequest, zoom: leafletMap.getZoom(), source_type: sourceType } try { await sendMLRequest('/infer_lisa', bodyRequest, leafletMap) console.log("sendMLStringRequest:: end") } catch (errGeojsonOutputOnMounted) { logErrorOnRequest("sendMLStringRequest", {sourceType, promptRequest, bodyRequest, errGeojsonOutputOnMounted}) } } export const sendMLArrayRequest = async ( leafletMap: LMap, promptRequest: Array, sourceType: SourceTileType = OpenStreetMap ) => { console.log("sendMLArrayRequest:: start") if (leafletMap.pm.globalDragModeEnabled()) { leafletMap.pm.disableGlobalDragMode() } if (leafletMap.pm.globalEditModeEnabled()) { leafletMap.pm.disableGlobalEditMode() } mapNavigationLocked.value = true const bodyRequest: IBodyLatLngPoints = { bbox: getExtentCurrentViewMapBBox(leafletMap), prompt: promptRequest, zoom: leafletMap.getZoom(), source_type: sourceType } try { await sendMLRequest('/infer_samgis', bodyRequest, leafletMap) console.log("sendMLArrayRequest:: end") } catch (errGeojsonOutputOnMounted) { logErrorOnRequest("sendMLArrayRequest", {sourceType, promptRequest, bodyRequest, errGeojsonOutputOnMounted}) } } const sendMLRequest = async(url: string, bodyRequest: IBodyLatLngPoints | IBodyLatLngWithStringPoints, leafletMap: LMap) => { const geojsonOutputOnMounted = await getGeoJSONRequest(bodyRequest, url) const featureNew = LeafletGeoJSON(geojsonOutputOnMounted) let now = new Date(Date.now()) let nowString = now.toLocaleString('it-it') let overlayMaps = new FeatureGroup([featureNew]) layerControlGroupLayersRef.value.addOverlay(overlayMaps, nowString) leafletMap.addLayer(featureNew) } export const getQueryParams = () => { const urlSearchParams = new URLSearchParams(window.location.search); const params = Object.fromEntries(urlSearchParams.entries()); const {source,...options} = params return {source, options} } export const applyFnToObjectWithinArray = (array: Array): Array => { let newArray = [] for (const el of array) { newArray.push(el.type === 'rectangle' ? getUpdatedRectangle(el) : getUpdatedPoint(el)) } return newArray } const getUpdatedPoint = (obj: IPointPrompt): IPointTable => { return { id: obj.id, data: obj.data, label: obj.label } } const getUpdatedRectangle = (obj: IRectanglePrompt): IRectangleTable => { return { id: obj.id, data_ne: obj.data.ne, data_sw: obj.data.sw, } } /** get a custom icon given a PNG path with its anchor/size values */ const getCustomIconMarker = ( iconUrlNoExt: string, shadowUrl = '/marker-shadow.png', iconSize: ArrayNumber = [25, 41], iconAnchor: ArrayNumber = [12, 41], popupAnchor: ArrayNumber = [1, -34], tooltipAnchor: ArrayNumber = [5, -25], shadowSize: ArrayNumber = [41, 41] ): icon => { return icon({ iconUrl: `${iconUrlNoExt}.png`, iconRetinaUrl: `${iconUrlNoExt}-2x.png`, shadowUrl, iconSize, iconAnchor, popupAnchor, shadowSize, tooltipAnchor }) } /** get an the leaflet editor geoman.io toolbar with the custom actions to draw/edit/move point and rectangle layers */ const getCustomGeomanActionsObject = ( actionName: string, descriptionAction: string, arrayActions: Array, customClassName: string ) => { return { name: actionName, block: 'custom', className: customClassName, title: descriptionAction, actions: arrayActions } } /** prepare the leaflet editor geoman.io toolbar with the custom actions to draw/edit/move point and rectangle layers */ export function setGeomanControls(localMap: LMap) { // leaflet geoman toolbar localMap.pm.addControls({ position: 'topleft', drawControls: false, rotateMode: false, cutPolygon: false, customControls: true }) const actionArray = [{ onClick(actionEvent: LEvented) { console.log('actionEvent:', typeof actionEvent, '|', actionEvent, '') }, name: 'actionName' }] const includeMarkerControl = localMap.pm.Toolbar.copyDrawControl('Marker', getCustomGeomanActionsObject( 'IncludeMarkerPrompt', 'Marker point that add recognition regions from SAM prompt requests', actionArray, 'control-icon leaflet-pm-icon-marker-include' ) ) // custom marker icon on map includeMarkerControl.drawInstance.setOptions({ markerStyle: { icon: getCustomIconMarker('/marker-icon-include') } }) const excludeMarkerControl = localMap.pm.Toolbar.copyDrawControl('Marker', getCustomGeomanActionsObject( 'ExcludeMarkerPrompt', 'Marker point that remove recognition regions from SAM prompt requests', actionArray, 'control-icon leaflet-pm-icon-marker-exclude' ) ) excludeMarkerControl.drawInstance.setOptions({ markerStyle: { icon: getCustomIconMarker('/marker-icon-exclude') } }) localMap.pm.Toolbar.copyDrawControl('Rectangle', { actions: actionArray, block: 'custom', name: 'RectanglePrompt', title: 'Rectangular recognition regions for SAM prompt requests' }) localMap.pm.setPathOptions({ color: "green", fillColor: "green", fillOpacity: 0.15, }) } /** get the selected rectangle layer bounding box coordinate */ export const getSelectedRectangleCoordinatesBBox = (leafletEvent: LEvented): BboxLatLng => { const { _northEast, _southWest } = leafletEvent.layer._bounds return { ne: new L.LatLng(_northEast.lat, _northEast.lng), sw: new L.LatLng(_southWest.lat, _southWest.lng) } } /** get the current selected point coordinate */ export const getSelectedPointCoordinate = (leafletEvent: LEvented): LatLng => { return leafletEvent.layer._latlng } /** get the current map bounding box coordinates */ export const getExtentCurrentViewMapBBox = (leafletMap: LMap): BboxLatLng => { const boundaries = leafletMap.getBounds() return { ne: boundaries.getNorthEast(), sw: boundaries.getSouthWest() } } /** send the ML request to the backend API through the cloudflare proxy function */ export const getGeoJSONRequest = async ( requestBody: IBodyLatLngPoints | IBodyLatLngWithStringPoints, urlApi: string ) => { responseMessageRef.value = waitingString responseMessageLisaRef.value = waitingString console.log(`getGeoJSONRequest urlApi: ${urlApi} ...`) const data = await fetch(urlApi, { method: 'POST', body: JSON.stringify(requestBody), headers: { 'Content-type': 'application/json' } }) try { console.log(`getGeoJSONRequest data.status: ${data.status} ...`) if (data.status === 200) { const output: Object = await data.json() try { const parsed = JSON.parse(output.body) const { geojson, n_predictions, n_shapes_geojson } = parsed.output try { console.log("getGeoJSONRequest parsed.output output_string:", parsed.output.output_string, "#") const { output_string } = parsed.output responseMessageLisaRef.value = output_string } catch(e) { console.log("getGeoJSONRequest parsed.output e:", e, "#") responseMessageLisaRef.value = 'error: check the logs' } const parsedGeojson = JSON.parse(geojson) durationRef.value = parsed.duration_run numberOfPolygonsRef.value = n_shapes_geojson numberOfPredictedMasksRef.value = n_predictions responseMessageRef.value = '' return parsedGeojson } catch (errParseOutput1) { console.error("errParseOutput1::", errParseOutput1) return String(errParseOutput1) } } else { const outputText = await data.text() console.error('getGeoJSONRequest => status not 200, outputText', outputText, '#') responseMessageRef.value = `error message response: ${outputText}...` } } catch (errorOtherData) { const statusText = data.statusText || 'no response or uncaught exception!' console.error( 'getGeoJSONRequest => data', data, 'statusText', statusText, 'errorOtherData', errorOtherData, '#' ) responseMessageRef.value = `error status response: ${statusText}...` } } /** populate a single point ML request prompt, by type (exclude or include), see type ExcludeIncludeLabelPrompt */ export const getPointPromptElement = (e: LEvented, elementType: ExcludeIncludeLabelPrompt): IPointPrompt|IRectanglePrompt => { const currentPointLayer: LatLng = getSelectedPointCoordinate(e) return { id: e.layer._leaflet_id, type: 'point', data: currentPointLayer, label: elementType } } /** populate a single rectangle ML request prompt */ export const getRectanglePromptElement = (e: LEvented) => { return { id: e.layer._leaflet_id, type: 'rectangle', data: getSelectedRectangleCoordinatesBBox(e) } } /** handle different event/layer types (rectangle, point: IncludeMarkerPrompt, ExcludeMarkerPrompt) */ const updateLayerOnCreateOrEditEvent = ( event: LEvented, getPopupContentPointFn: (arg0: LEvented, arg1: number) => HTMLDivElement, promptsArrayRef: Ref) => { responseMessageRef.value = '-' if (event.shape === 'IncludeMarkerPrompt' || event.shape === 'ExcludeMarkerPrompt') { const labelPoint = Number(excludeIncludeLabelPrompt[event.shape]) const div = getPopupContentPointFn(event, labelPoint) event.layer.bindPopup(div).openPopup() promptsArrayRef.value.push(getPointPromptElement(event, labelPoint)) } if (event.shape === 'RectanglePrompt') { event.layer.bindPopup(`id:${event.layer._leaflet_id}.`).openPopup() promptsArrayRef.value.push(getRectanglePromptElement(event)) } } /** listen on the leaflet editor geoman.io events and update its layer properties within the promptsArrayRef vue ref */ export const updateMapData = ( localMap: LMap, getPopupContentPointFn: (arg0: LEvented, arg1: number) => HTMLDivElement, promptsArrayRef: Ref ) => { localMap.on('pm:create', (e: LEvented) => { updateLayerOnCreateOrEditEvent(e, getPopupContentPointFn, promptsArrayRef) // listen to changes on the new layer and update its object within promptsArrayRef e.layer.on('pm:edit', function(newEvent: LEvented) { promptsArrayRef.value = removeEventFromArrayByIndex(promptsArrayRef.value, newEvent) updateLayerOnCreateOrEditEvent(e, getPopupContentPointFn, promptsArrayRef) }); }) localMap.on('pm:remove', (e: LEvented) => { responseMessageRef.value = '-' promptsArrayRef.value = removeEventFromArrayByIndex(promptsArrayRef.value, e) }) } /** remove the selected layer from the ML request array prompt */ const removeEventFromArrayByIndex = (arr: Array, e: LEvented) => { return arr.filter((el: LEvented) => { return el.id != e.layer._leaflet_id }) }