diff --git a/webapp/packages/plugin-gis-viewer/package.json b/webapp/packages/plugin-gis-viewer/package.json index 5a7fe7bf78..f302d6d720 100644 --- a/webapp/packages/plugin-gis-viewer/package.json +++ b/webapp/packages/plugin-gis-viewer/package.json @@ -26,6 +26,7 @@ "geojson": "^0.5.0", "leaflet": "^1.9.4", "mobx-react-lite": "^4.0.5", + "proj4": "^2.10.0", "react": "^18.2.0", "react-leaflet": "^4.2.1", "reshadow": "^0.0.1", @@ -37,7 +38,9 @@ "@types/react": "^18.2.42", "@types/react-leaflet": "~3.0.0", "@types/wellknown": "~0.5.8", + "@types/proj4": "^2.5.5", "leaflet": "^1.9.4", - "typescript": "^5.3.2" + "typescript": "^5.3.2", + "typescript-plugin-css-modules": "^5.0.2" } } diff --git a/webapp/packages/plugin-gis-viewer/src/CrsInput.m.css b/webapp/packages/plugin-gis-viewer/src/CrsInput.m.css new file mode 100644 index 0000000000..be2fb54a9d --- /dev/null +++ b/webapp/packages/plugin-gis-viewer/src/CrsInput.m.css @@ -0,0 +1,10 @@ +.root { + display: inline-flex; + align-items: center; + font-size: 12px; +} + +.combobox { + width: 120px; + flex: 0 0 auto; +} diff --git a/webapp/packages/plugin-gis-viewer/src/CrsInput.tsx b/webapp/packages/plugin-gis-viewer/src/CrsInput.tsx index 9ea822cff5..999f23b088 100644 --- a/webapp/packages/plugin-gis-viewer/src/CrsInput.tsx +++ b/webapp/packages/plugin-gis-viewer/src/CrsInput.tsx @@ -1,40 +1,26 @@ -import styled, { css } from 'reshadow'; - +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ import { Combobox } from '@cloudbeaver/core-blocks'; +import classes from './CrsInput.m.css'; import type { CrsKey } from './LeafletMap'; -const styles = css` - root { - display: inline-flex; - align-items: center; - font-size: 12px; - } - - label { - margin-right: 4px; - flex-grow: 0; - flex-shrink: 1; - } - - Combobox { - width: 120px; - flex: 0 0 auto; - } -`; - interface Props { value: CrsKey; onChange: (value: CrsKey) => void; } -const items: CrsKey[] = ['Simple', 'EPSG3395', 'EPSG3857', 'EPSG4326', 'EPSG900913']; +const items: CrsKey[] = ['Simple', 'EPSG:3395', 'EPSG:3857', 'EPSG:4326', 'EPSG:900913']; export function CrsInput(props: Props) { - return styled(styles)( - - - - , + return ( +
+ +
); } diff --git a/webapp/packages/plugin-gis-viewer/src/GISValuePresentation.m.css b/webapp/packages/plugin-gis-viewer/src/GISValuePresentation.m.css new file mode 100644 index 0000000000..383c3893fa --- /dev/null +++ b/webapp/packages/plugin-gis-viewer/src/GISValuePresentation.m.css @@ -0,0 +1,16 @@ +.root { + display: flex; + flex-direction: column; + width: 100%; +} + +.map { + flex: 1 1 auto; + border-radius: var(--theme-group-element-radius); + overflow: hidden; +} + +.toolbar { + margin-top: 8px; + flex: 0 0 auto; +} diff --git a/webapp/packages/plugin-gis-viewer/src/GISValuePresentation.tsx b/webapp/packages/plugin-gis-viewer/src/GISValuePresentation.tsx index 6095e47457..09968cf155 100644 --- a/webapp/packages/plugin-gis-viewer/src/GISValuePresentation.tsx +++ b/webapp/packages/plugin-gis-viewer/src/GISValuePresentation.tsx @@ -6,9 +6,9 @@ * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import { useCallback, useMemo, useState } from 'react'; -import styled, { css } from 'reshadow'; -import wellknown from 'wellknown'; +import proj4 from 'proj4'; +import { useCallback, useState } from 'react'; +import wellknown, { GeoJSONGeometry } from 'wellknown'; import { TextPlaceholder, useTranslate } from '@cloudbeaver/core-blocks'; import { @@ -21,42 +21,56 @@ import { } from '@cloudbeaver/plugin-data-viewer'; import { CrsInput } from './CrsInput'; +import classes from './GISValuePresentation.m.css'; import { CrsKey, IAssociatedValue, IGeoJSONFeature, LeafletMap } from './LeafletMap'; import { ResultSetGISAction } from './ResultSetGISAction'; -function getCrsKey(feature?: IGeoJSONFeature): CrsKey { - switch (feature?.properties.srid) { +proj4.defs('EPSG:3395', '+title=World Mercator +proj=merc +lon_0=0 +k=1 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs'); + +function getCrsKey(srid: number): CrsKey { + switch (srid) { case 3857: - return 'EPSG3857'; + return 'EPSG:3857'; case 4326: - return 'EPSG4326'; + return 'EPSG:4326'; case 3395: - return 'EPSG3395'; + return 'EPSG:3395'; case 900913: - return 'EPSG900913'; + return 'EPSG:900913'; default: - return 'EPSG3857'; + return 'EPSG:4326'; } } -const styles = css` - root { - display: flex; - flex-direction: column; - width: 100%; +const DEFAULT_CRS = 'EPSG:3857'; +const DEFAULT_TRANSFORM_CRS = 'EPSG:4326'; + +function getTransformedGeometry(from: CrsKey, to: CrsKey, geometry: GeoJSONGeometry): GeoJSONGeometry { + if (geometry.type === 'Point') { + return { ...geometry, coordinates: proj4(from, to, geometry.coordinates) }; + } + + if (geometry.type === 'MultiPoint' || geometry.type === 'LineString') { + return { ...geometry, coordinates: geometry.coordinates.map(point => proj4(from, to, point)) }; } - map { - flex: 1 1 auto; - border-radius: var(--theme-group-element-radius); - overflow: hidden; + if (geometry.type === 'MultiLineString' || geometry.type === 'Polygon') { + return { ...geometry, coordinates: geometry.coordinates.map(line => line.map(point => proj4(from, to, point))) }; } - toolbar { - margin-top: 8px; - flex: 0 0 auto; + if (geometry.type === 'MultiPolygon') { + return { + ...geometry, + coordinates: geometry.coordinates.map(polygon => polygon.map(line => line.map(point => proj4(from, to, point)))), + }; } -`; + + if (geometry.type === 'GeometryCollection') { + return { ...geometry, geometries: geometry.geometries.map(geometry => getTransformedGeometry(from, to, geometry)) }; + } + + return geometry; +} interface Props { model: IDatabaseDataModel; @@ -70,9 +84,15 @@ export const GISValuePresentation = observer(function GISValuePresentatio const gis = model.source.getAction(resultIndex, ResultSetGISAction); const view = model.source.getAction(resultIndex, ResultSetViewAction); + const parsedGISData: IGeoJSONFeature[] = []; const activeElements = selection.getActiveElements(); + const firstActiveElement = activeElements[0]; + const firstActiveCell = firstActiveElement ? gis.getCellValue(firstActiveElement) : null; + const initialCrs: CrsKey = firstActiveCell?.srid ? getCrsKey(firstActiveCell.srid) : DEFAULT_CRS; - const parsedGISData: IGeoJSONFeature[] = []; + const [crs, setCrs] = useState(null); + + const currentCrs = crs ?? initialCrs; for (const cell of activeElements) { const cellValue = gis.getCellValue(cell); @@ -81,15 +101,24 @@ export const GISValuePresentation = observer(function GISValuePresentatio continue; } + const text = cellValue.mapText || cellValue.text; + try { - const parsedCellValue = wellknown.parse(cellValue.mapText || cellValue.text); + const parsedCellValue = wellknown.parse(text); + if (!parsedCellValue) { continue; } - parsedGISData.push({ type: 'Feature', geometry: parsedCellValue, properties: { associatedCell: cell, srid: cellValue.srid } }); + const from = cellValue.srid === 0 ? DEFAULT_TRANSFORM_CRS : getCrsKey(cellValue.srid); + + parsedGISData.push({ + type: 'Feature', + geometry: currentCrs === 'Simple' ? parsedCellValue : getTransformedGeometry(from, currentCrs, parsedCellValue), + properties: { associatedCell: cell, srid: cellValue.srid }, + }); } catch (exception: any) { - console.error(`Failed to parse "${cellValue.mapText || cellValue.text}" value.`); + console.error(`Failed to parse "${text}" value.`); console.error(exception); } } @@ -119,21 +148,18 @@ export const GISValuePresentation = observer(function GISValuePresentatio [view], ); - const defaultCrsKey = getCrsKey(parsedGISData[0]); - const [crsKey, setCrsKey] = useState(defaultCrsKey); - if (!parsedGISData.length) { return {translate('gis_presentation_placeholder')}; } - return styled(styles)( - - - - - - - - , + return ( +
+
+ +
+
+ +
+
); }); diff --git a/webapp/packages/plugin-gis-viewer/src/LeafletMap.tsx b/webapp/packages/plugin-gis-viewer/src/LeafletMap.tsx index 8afb863afd..ea33764470 100644 --- a/webapp/packages/plugin-gis-viewer/src/LeafletMap.tsx +++ b/webapp/packages/plugin-gis-viewer/src/LeafletMap.tsx @@ -10,8 +10,7 @@ import type geojson from 'geojson'; import leaflet from 'leaflet'; import { useCallback, useEffect, useState } from 'react'; -import { GeoJSON, LayersControl, MapContainer, TileLayer } from 'react-leaflet'; -import type { TileLayerProps } from 'react-leaflet'; +import { GeoJSON, LayersControl, MapContainer, TileLayer, type TileLayerProps } from 'react-leaflet'; import styled, { css } from 'reshadow'; import { useSplit, useTranslate } from '@cloudbeaver/core-blocks'; @@ -39,7 +38,7 @@ interface IBaseTile extends TileLayerProps { checked?: boolean; } -export type CrsKey = 'Simple' | 'EPSG3857' | 'EPSG4326' | 'EPSG3395' | 'EPSG900913'; +export type CrsKey = 'Simple' | 'EPSG:3857' | 'EPSG:4326' | 'EPSG:3395' | 'EPSG:900913'; interface Props { geoJSON: IGeoJSONFeature[]; @@ -92,13 +91,13 @@ function getCRS(crsKey: CrsKey): leaflet.CRS { switch (crsKey) { case 'Simple': return leaflet.CRS.Simple; - case 'EPSG3857': + case 'EPSG:3857': return leaflet.CRS.EPSG3857; - case 'EPSG4326': + case 'EPSG:4326': return leaflet.CRS.EPSG4326; - case 'EPSG3395': + case 'EPSG:3395': return leaflet.CRS.EPSG3395; - case 'EPSG900913': + case 'EPSG:900913': return leaflet.CRS.EPSG900913; default: return leaflet.CRS.EPSG3857; @@ -175,20 +174,14 @@ export const LeafletMap: React.FC = function LeafletMap({ geoJSON, crsKey useEffect(() => { if (mapRef) { mapRef.invalidateSize(); - - if (mapRef.options.crs?.code !== crs.code) { - const center = mapRef.getCenter(); - mapRef.options.crs = crs; - mapRef.setView(center); - } } - }, [split.state.isResizing, split.state.mode, crs, mapRef]); + }, [split.state.isResizing, split.state.mode, mapRef]); return styled( styles, baseStyles, )( - +