Spaces:
Sleeping
Sleeping
alessandro trinca tornidor
[feat] PageLisaMap/PagePredictionMap: add inference polygons as overlay, refactor code
d52c87f
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<IPointPrompt | IRectanglePrompt>, 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<IPointPrompt | IRectanglePrompt>): Array<IPointTable | IRectangleTable> => { | |
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<object>, 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<LEvented>, e: LEvented) => { | |
return arr.filter((el: LEvented) => { | |
return el.id != e.layer._leaflet_id | |
}) | |
} | |