mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
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:
@@ -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={[]}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user