CB-4504 transform coordinates according to the chosen srid (#2331)

* CB-4504 transform coordinates according to the chosen srid

* CB-4504 add licence

* CB-4504 move types to dev deps

---------

Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
This commit is contained in:
alex
2024-01-30 17:22:52 +01:00
committed by GitHub
co-authored by Daria Marutkina
parent 7a45f2feea
commit cd7ffe6c2e
7 changed files with 140 additions and 83 deletions
@@ -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"
}
}
@@ -0,0 +1,10 @@
.root {
display: inline-flex;
align-items: center;
font-size: 12px;
}
.combobox {
width: 120px;
flex: 0 0 auto;
}
@@ -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)(
<root>
<label>CRS:</label>
<Combobox items={items} value={props.value} onSelect={props.onChange} />
</root>,
return (
<div className={classes.root}>
<Combobox className={classes.combobox} items={items} value={props.value} onSelect={props.onChange} />
</div>
);
}
@@ -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;
}
@@ -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) };
}
map {
flex: 1 1 auto;
border-radius: var(--theme-group-element-radius);
overflow: hidden;
if (geometry.type === 'MultiPoint' || geometry.type === 'LineString') {
return { ...geometry, coordinates: geometry.coordinates.map(point => proj4(from, to, point)) };
}
toolbar {
margin-top: 8px;
flex: 0 0 auto;
if (geometry.type === 'MultiLineString' || geometry.type === 'Polygon') {
return { ...geometry, coordinates: geometry.coordinates.map(line => line.map(point => proj4(from, to, point))) };
}
`;
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<any, IDatabaseResultSet>;
@@ -70,9 +84,15 @@ export const GISValuePresentation = observer<Props>(function GISValuePresentatio
const gis = model.source.getAction(resultIndex, ResultSetGISAction);
const view = model.source.getAction(resultIndex, ResultSetViewAction);
const activeElements = selection.getActiveElements();
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 [crs, setCrs] = useState<CrsKey | null>(null);
const currentCrs = crs ?? initialCrs;
for (const cell of activeElements) {
const cellValue = gis.getCellValue(cell);
@@ -81,15 +101,24 @@ export const GISValuePresentation = observer<Props>(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<Props>(function GISValuePresentatio
[view],
);
const defaultCrsKey = getCrsKey(parsedGISData[0]);
const [crsKey, setCrsKey] = useState(defaultCrsKey);
if (!parsedGISData.length) {
return <TextPlaceholder>{translate('gis_presentation_placeholder')}</TextPlaceholder>;
}
return styled(styles)(
<root>
<map>
<LeafletMap key={crsKey} geoJSON={parsedGISData} crsKey={crsKey} getAssociatedValues={getAssociatedValues} />
</map>
<toolbar>
<CrsInput value={crsKey} onChange={setCrsKey} />
</toolbar>
</root>,
return (
<div className={classes.root}>
<div className={classes.map}>
<LeafletMap key={currentCrs} geoJSON={parsedGISData} crsKey={currentCrs} getAssociatedValues={getAssociatedValues} />
</div>
<div className={classes.toolbar}>
<CrsInput value={currentCrs} onChange={setCrs} />
</div>
</div>
);
});
@@ -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<Props> = 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,
)(
<MapContainer ref={setMapRef} crs={crs} zoom={12}>
<MapContainer ref={setMapRef} crs={leaflet.CRS.EPSG3857} zoom={12}>
<GeoJSON
// data is not optional property, see react-leaflet.d.ts
// data={[]}
+23
View File
@@ -4796,6 +4796,11 @@
dependencies:
postcss "^8.0.0"
"@types/proj4@^2.5.5":
version "2.5.5"
resolved "https://registry.yarnpkg.com/@types/proj4/-/proj4-2.5.5.tgz#044d53782dc75f20335577ca3af2643962a56980"
integrity sha512-y4tHUVVoMEOm2nxRLQ2/ET8upj/pBmoutGxFw2LZJTQWPgWXI+cbxVEUFFmIzr/bpFR83hGDOTSXX6HBeObvZA==
"@types/prop-types@*":
version "15.7.11"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.11.tgz#2596fb352ee96a1379c657734d4b913a613ad563"
@@ -12442,6 +12447,11 @@ methods@~1.1.2:
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==
mgrs@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/mgrs/-/mgrs-1.0.0.tgz#fb91588e78c90025672395cb40b25f7cd6ad1829"
integrity sha512-awNbTOqCxK1DBGjalK3xqWIstBZgN6fxsMSiXLs9/spqWkF2pAhb2rrYCFSsr1/tT7PhcDGjZndG8SWYn0byYA==
micromark-core-commonmark@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-2.0.0.tgz#50740201f0ee78c12a675bf3e68ffebc0bf931a3"
@@ -15112,6 +15122,14 @@ process-nextick-args@~2.0.0:
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
proj4@^2.10.0:
version "2.10.0"
resolved "https://registry.yarnpkg.com/proj4/-/proj4-2.10.0.tgz#f6f5391b0f1b1fa4518d9ead150ea6b66e7ce9de"
integrity sha512-0eyB8h1PDoWxucnq88/EZqt7UZlvjhcfbXCcINpE7hqRN0iRPWE/4mXINGulNa/FAvK+Ie7F+l2OxH/0uKV36A==
dependencies:
mgrs "1.0.0"
wkt-parser "^1.3.3"
promise-all-reject-late@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/promise-all-reject-late/-/promise-all-reject-late-1.0.1.tgz#f8ebf13483e5ca91ad809ccc2fcf25f26f8643c2"
@@ -18316,6 +18334,11 @@ wildcard@^2.0.0:
resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67"
integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==
wkt-parser@^1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/wkt-parser/-/wkt-parser-1.3.3.tgz#46b4e3032dd9c86907f7e630b57e3c6ea2bb772b"
integrity sha512-ZnV3yH8/k58ZPACOXeiHaMuXIiaTk1t0hSUVisbO0t4RjA5wPpUytcxeyiN2h+LZRrmuHIh/1UlrR9e7DHDvTw==
wordwrap@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"