From 68253d5b2681a357adcecab1dc801750901872a5 Mon Sep 17 00:00:00 2001 From: Gabriele Dal Cengio Date: Wed, 15 Nov 2023 16:07:30 -0800 Subject: [PATCH 1/4] Allow uploading and display of lines in KMLs --- api/src/paths/admin-defined-shapes.ts | 103 +++++++++++++++----------- api/src/utils/kml-import.ts | 6 ++ 2 files changed, 65 insertions(+), 44 deletions(-) diff --git a/api/src/paths/admin-defined-shapes.ts b/api/src/paths/admin-defined-shapes.ts index 7aab2645a..69b1ab092 100644 --- a/api/src/paths/admin-defined-shapes.ts +++ b/api/src/paths/admin-defined-shapes.ts @@ -1,21 +1,21 @@ 'use strict'; -import {RequestHandler} from 'express'; -import {Operation} from 'express-openapi'; -import {getDBConnection} from '../database/db'; -import {getLogger} from '../utils/logger'; -import {SQLStatement} from 'sql-template-strings'; +import { RequestHandler } from 'express'; +import { Operation } from 'express-openapi'; +import { getDBConnection } from '../database/db'; +import { getLogger } from '../utils/logger'; +import { SQLStatement } from 'sql-template-strings'; import { deleteAdministrativelyDefinedShapesSQL, getAdministrativelyDefinedShapesSQL } from '../queries/admin-defined-shapes'; -import {atob} from 'js-base64'; -import {QueryResult} from 'pg'; -import {FeatureCollection} from 'geojson'; -import {GeoJSONFromKML, KMZToKML, sanitizeGeoJSON} from '../utils/kml-import'; -import {InvasivesRequest} from '../utils/auth-utils'; -import {ALL_ROLES, SECURITY_ON} from '../constants/misc'; -import {simplifyGeojson} from '../utils/map-shaper-util'; +import { atob } from 'js-base64'; +import { QueryResult } from 'pg'; +import { FeatureCollection } from 'geojson'; +import { GeoJSONFromKML, KMZToKML, sanitizeGeoJSON } from '../utils/kml-import'; +import { InvasivesRequest } from '../utils/auth-utils'; +import { ALL_ROLES, SECURITY_ON } from '../constants/misc'; +import { simplifyGeojson } from '../utils/map-shaper-util'; const defaultLog = getLogger('admin-defined-shapes'); @@ -27,10 +27,10 @@ GET.apiDoc = { description: 'Fetches a GeoJSON object to display boundaries of administratively-defined shapes (KML uploads)', security: SECURITY_ON ? [ - { - Bearer: ALL_ROLES - } - ] + { + Bearer: ALL_ROLES + } + ] : [], responses: { 200: { @@ -59,12 +59,12 @@ GET.apiDoc = { POST.apiDoc = { description: 'Creates new Administratively-defined shapes from KML/KMZ data', security: SECURITY_ON - ? [ - { - Bearer: ALL_ROLES - } - ] - : [], + ? [ + { + Bearer: ALL_ROLES + } + ] + : [], requestBody: { description: 'Uploaded KML/KMZ file', content: { @@ -103,10 +103,10 @@ DELETE.apiDoc = { description: 'deletes new Administratively-defined shapes from KML/KMZ data', security: SECURITY_ON ? [ - { - Bearer: ALL_ROLES - } - ] + { + Bearer: ALL_ROLES + } + ] : [], requestBody: { description: 'Delete KML/KMZ file', @@ -174,25 +174,40 @@ function getAdministrativelyDefinedShapes(): RequestHandler { // parse the rows from the response const rows: any[] = (response && response.rows) || []; - // terrible nesting, but returned KMLs should be a small set so should be fine for now + //edited, still could be much more efficient for (const row of rows) { - let newFeatureArr = []; - for (const feature of row.geojson.features) { - if (feature !== null && feature.coordinates !== null) { - for (const multipolygon of feature.coordinates) { - let convertedFeature = { - type: 'Feature', - properties: {}, - geometry: { - type: 'Polygon', - coordinates: multipolygon - } - }; - - newFeatureArr.push(convertedFeature); + const newFeatureArr = []; + row?.geojson?.features?.forEach((feature) => { + if (feature === null || feature?.coordinates === null) return; + + for (let coords of feature.coordinates) { + let shape; + switch (feature?.type) { + case 'MultiLineString': + shape = { + type: 'Feature', + properties: {}, + geometry: { + type: 'LineString', + coordinates: coords + } + }; + break; + case 'MultiPolygon': + shape = { + type: 'Feature', + properties: {}, + geometry: { + type: 'Polygon', + coordinates: coords + } + }; + break; } + newFeatureArr.push(shape); } - } + }); + row.geojson.features = newFeatureArr; } @@ -227,7 +242,7 @@ function getAdministrativelyDefinedShapes(): RequestHandler { function uploadShape(): RequestHandler { return async (req: InvasivesRequest, res) => { const user_id = req.authContext.user.user_id; - const data = {...req.body}; + const data = { ...req.body }; const title = data.title; let geoJSON: FeatureCollection; @@ -358,7 +373,7 @@ function deleteShape(): RequestHandler { code: 200 }); } catch (error) { - defaultLog.debug({label: 'deleteAdministrativelyDefinedShapes', message: 'error', error}); + defaultLog.debug({ label: 'deleteAdministrativelyDefinedShapes', message: 'error', error }); return res.status(500).json({ message: 'Failed to delete administratively defined shape', request: req.body, diff --git a/api/src/utils/kml-import.ts b/api/src/utils/kml-import.ts index 717c26176..9f50c435b 100644 --- a/api/src/utils/kml-import.ts +++ b/api/src/utils/kml-import.ts @@ -68,6 +68,12 @@ function sanitizeGeoJSON(data: FeatureCollection): FeatureCollection { geometry: feature.geometry, properties: {} }; + } else if (feature.geometry.type === 'LineString') { + return { + type: feature.type, + geometry: feature.geometry, + properties: {} + }; } }); From a8821f2a9d1657e63304d114859ffd02ed8904ba Mon Sep 17 00:00:00 2001 From: Gabriele Dal Cengio Date: Fri, 17 Nov 2023 09:49:10 -0800 Subject: [PATCH 2/4] Allow KML points to display as circle markers --- api/src/paths/admin-defined-shapes.ts | 10 ++++++++++ api/src/utils/kml-import.ts | 12 +++++------- appv2/src/UI/Map/LayerPickerBasic.tsx | 9 +++++++-- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/api/src/paths/admin-defined-shapes.ts b/api/src/paths/admin-defined-shapes.ts index 69b1ab092..1c7b74832 100644 --- a/api/src/paths/admin-defined-shapes.ts +++ b/api/src/paths/admin-defined-shapes.ts @@ -183,6 +183,16 @@ function getAdministrativelyDefinedShapes(): RequestHandler { for (let coords of feature.coordinates) { let shape; switch (feature?.type) { + case 'MultiPoint': + shape = { + type: 'Feature', + properties: {}, + geometry: { + type: 'Point', + coordinates: coords + } + }; + break; case 'MultiLineString': shape = { type: 'Feature', diff --git a/api/src/utils/kml-import.ts b/api/src/utils/kml-import.ts index 9f50c435b..6bcf38d5a 100644 --- a/api/src/utils/kml-import.ts +++ b/api/src/utils/kml-import.ts @@ -62,13 +62,11 @@ function sanitizeGeoJSON(data: FeatureCollection): FeatureCollection { // filter out non-polygon features (V1) const newFeatures = data.features.map((feature) => { - if (feature.geometry.type === 'Polygon') { - return { - type: feature.type, - geometry: feature.geometry, - properties: {} - }; - } else if (feature.geometry.type === 'LineString') { + if ( + feature.geometry.type === 'Polygon' || + feature.geometry.type === 'LineString' || + feature.geometry.type === 'Point' + ) { return { type: feature.type, geometry: feature.geometry, diff --git a/appv2/src/UI/Map/LayerPickerBasic.tsx b/appv2/src/UI/Map/LayerPickerBasic.tsx index 601b4e894..bf6725c14 100644 --- a/appv2/src/UI/Map/LayerPickerBasic.tsx +++ b/appv2/src/UI/Map/LayerPickerBasic.tsx @@ -1,4 +1,4 @@ -import { LatLngBoundsExpression, LatLngExpression, rectangle } from 'leaflet'; +import { circleMarker, LatLngBoundsExpression, LatLngExpression } from 'leaflet'; import React from 'react'; import { GeoJSON, @@ -234,7 +234,12 @@ const dispatch = useDispatch(); return ( - + { + return circleMarker(latlng, { + radius: 2 + }); + }}/> ); From 0eddc4ac126abef84f71a78067d16282f181bb3e Mon Sep 17 00:00:00 2001 From: Gabriele Dal Cengio Date: Wed, 22 Nov 2023 16:18:09 -0800 Subject: [PATCH 3/4] Migration for geog column to allow more than polygons --- .../src/migrations/0077_geog_column_generic.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 database/src/migrations/0077_geog_column_generic.ts diff --git a/database/src/migrations/0077_geog_column_generic.ts b/database/src/migrations/0077_geog_column_generic.ts new file mode 100644 index 000000000..23efbf845 --- /dev/null +++ b/database/src/migrations/0077_geog_column_generic.ts @@ -0,0 +1,18 @@ +import { Knex } from 'knex'; + +export async function up(knex: Knex): Promise { + await knex.raw( + ` + set search_path=invasivesbc,public; + + ALTER TABLE admin_defined_shapes ALTER COLUMN geog TYPE geography(Geometry,4326); +` + ); +} + +export async function down(knex: Knex): Promise { + await knex.raw(` + set search_path=invasivesbc,public; + + `); +} From 28c59d48258100a5de51c6705640193267ecef7e Mon Sep 17 00:00:00 2001 From: Gabriele Dal Cengio Date: Wed, 22 Nov 2023 16:29:19 -0800 Subject: [PATCH 4/4] Handle KML with point with other geometries mixed together to deal with mapshaper incompatible geos --- api/src/paths/admin-defined-shapes.ts | 88 ++++++++++++++++----------- api/src/utils/map-shaper-util.ts | 43 ++++++++----- 2 files changed, 80 insertions(+), 51 deletions(-) diff --git a/api/src/paths/admin-defined-shapes.ts b/api/src/paths/admin-defined-shapes.ts index 1c7b74832..77b8726d5 100644 --- a/api/src/paths/admin-defined-shapes.ts +++ b/api/src/paths/admin-defined-shapes.ts @@ -178,43 +178,59 @@ function getAdministrativelyDefinedShapes(): RequestHandler { for (const row of rows) { const newFeatureArr = []; row?.geojson?.features?.forEach((feature) => { - if (feature === null || feature?.coordinates === null) return; - - for (let coords of feature.coordinates) { - let shape; - switch (feature?.type) { - case 'MultiPoint': - shape = { - type: 'Feature', - properties: {}, - geometry: { - type: 'Point', - coordinates: coords - } - }; - break; - case 'MultiLineString': - shape = { - type: 'Feature', - properties: {}, - geometry: { - type: 'LineString', - coordinates: coords - } - }; - break; - case 'MultiPolygon': - shape = { - type: 'Feature', - properties: {}, - geometry: { - type: 'Polygon', - coordinates: coords - } - }; - break; + if (feature === null) return; + + if (feature.type === 'GeometryCollection') { + if (feature?.geometries === null) return; + for (let geometry of feature.geometries) { + let shape = { + type: 'Feature', + properties: {}, + geometry: { + type: geometry.type, + coordinates: geometry.coordinates + } + }; + newFeatureArr.push(shape); + } + } else { + if (feature?.coordinates === null) return; + for (let coords of feature.coordinates) { + let shape; + switch (feature?.type) { + case 'MultiPoint': + shape = { + type: 'Feature', + properties: {}, + geometry: { + type: 'Point', + coordinates: coords + } + }; + break; + case 'MultiLineString': + shape = { + type: 'Feature', + properties: {}, + geometry: { + type: 'LineString', + coordinates: coords + } + }; + break; + case 'MultiPolygon': + shape = { + type: 'Feature', + properties: {}, + geometry: { + type: 'Polygon', + coordinates: coords + } + }; + break; + } + newFeatureArr.push(shape); } - newFeatureArr.push(shape); } }); diff --git a/api/src/utils/map-shaper-util.ts b/api/src/utils/map-shaper-util.ts index 6a79ffcfd..4136953fc 100644 --- a/api/src/utils/map-shaper-util.ts +++ b/api/src/utils/map-shaper-util.ts @@ -29,30 +29,43 @@ async function simplifyGeojson(data, percentage, returnCallback) { { 'in.json': data }, function (err, output) { if (output) { - let json = output['out.json']; - let parsed = JSON.parse(json); - let parsedEdit = JSON.parse(JSON.stringify(parsed)); - - delete parsedEdit.features; - let newFeatures = parsed?.features?.map((feature) => { - if (typeof feature.properties === 'object' && feature.properties !== null) { - return feature; - } else { - return { ...feature, properties: {} }; - } + let jsonArr = []; + jsonArr.push(output['out.json']); + if (!jsonArr[0]) { + jsonArr = []; + jsonArr.push(output['out1.json']); + jsonArr.push(output['out2.json']); + } + + let totalEdit = { + type: 'FeatureCollection', + features: [] + }; + + jsonArr.forEach((json) => { + let parsed = JSON.parse(json); + + let newFeatures = parsed?.features?.map((feature) => { + if (typeof feature.properties === 'object' && feature.properties !== null) { + return feature; + } else { + return { ...feature, properties: {} }; + } + }); + + totalEdit.features.push(...newFeatures); }); - parsedEdit.features = [...newFeatures]; - returnCallback(JSON.stringify(parsedEdit)); + returnCallback(JSON.stringify(totalEdit)); } else { - defaultLog.error({message: 'unspecified failure'}); + defaultLog.error({ message: 'unspecified failure' }); return data; } } ); } catch (e) { - defaultLog.error({error: e}); + defaultLog.error({ error: e }); } }