Skip to content

Commit

Permalink
CB-4504 transform coordinates according to the chosen srid (#2331)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
devnaumov and dariamarutkina authored Jan 30, 2024
1 parent 7a45f2f commit cd7ffe6
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 82 deletions.
5 changes: 4 additions & 1 deletion webapp/packages/plugin-gis-viewer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
}
}
10 changes: 10 additions & 0 deletions webapp/packages/plugin-gis-viewer/src/CrsInput.m.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.root {
display: inline-flex;
align-items: center;
font-size: 12px;
}

.combobox {
width: 120px;
flex: 0 0 auto;
}
40 changes: 13 additions & 27 deletions webapp/packages/plugin-gis-viewer/src/CrsInput.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
}
16 changes: 16 additions & 0 deletions webapp/packages/plugin-gis-viewer/src/GISValuePresentation.m.css
Original file line number Diff line number Diff line change
@@ -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;
}
104 changes: 65 additions & 39 deletions webapp/packages/plugin-gis-viewer/src/GISValuePresentation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<any, IDatabaseResultSet>;
Expand All @@ -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 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<CrsKey | null>(null);

const currentCrs = crs ?? initialCrs;

for (const cell of activeElements) {
const cellValue = gis.getCellValue(cell);
Expand All @@ -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);
}
}
Expand Down Expand Up @@ -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>
);
});
23 changes: 8 additions & 15 deletions webapp/packages/plugin-gis-viewer/src/LeafletMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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[];
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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={[]}
Expand Down
23 changes: 23 additions & 0 deletions webapp/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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==

[email protected]:
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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit cd7ffe6

Please sign in to comment.