diff --git a/.gitea/workflows/verify.yml b/.gitea/workflows/verify.yml new file mode 100644 index 0000000..b84a7cb --- /dev/null +++ b/.gitea/workflows/verify.yml @@ -0,0 +1,39 @@ +name: Verify + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +concurrency: + group: verify-${{ gitea.repository }}-${{ gitea.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-latest + timeout-minutes: 45 + env: + CI: "true" + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "22" + cache: npm + - name: Select declared npm version + run: npm install --global npm@11.17.0 + - name: Install dependencies + run: npm ci + - name: Audit runtime dependencies + run: npm audit --omit=dev --audit-level=moderate + - name: Check, test, and build + run: npm run check + - name: Install browser engines + run: npx playwright install --with-deps chromium firefox webkit + - name: Browser tests + run: npm run test:browser diff --git a/CHANGELOG.md b/CHANGELOG.md index cc16c8e..0fc2f59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.2.0 - 2026-09-02 + +- Added validated full-GeoJSON feature editing, append/delete, nested coordinate addressing, line-track reversal, and GPX-derived track inventories. +- Added a local great-circle distance/bearing/midpoint ruler and Toolbox format/capability metadata while retaining the tile-free, WGS 84-only boundary. + ## 0.1.0 - 2026-09-01 - Added the initial local-first Geo Tools workbench. diff --git a/README.md b/README.md index d3f1ae6..26ac563 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,24 @@ # Geo Tools -Inspect, convert and analyse geospatial files locally in the browser. +Inspect, edit, measure and convert geospatial files locally in the browser. Geo Tools is a standalone local-first application in the [add·ideas Toolbox](https://git.add-ideas.de/lotobo/toolbox-portal). Inputs are processed in the browser and are not uploaded. ## Version 0.1 scope -- Bounded GeoJSON, GPX, KML and coordinate-CSV parsing into a small common model +- Bounded GeoJSON, GPX, KML and coordinate-CSV parsing into a common model with Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon and nested GeometryCollection support - WGS 84 bounds, point counts, great-circle LineString distance and elevation gain/loss - Explicit lossy conversion among the four supported formats - Iterative Douglas–Peucker LineString simplification and decimal-degree/DMS conversion - A coordinate-only local sketch with no map tiles or projection service +- Full-model GeoJSON geometry replacement, append/delete, nested coordinate editing and line-direction reversal with validation after every edit +- GPX-derived track inventory plus a local two-point distance, initial-bearing and midpoint ruler -The common model intentionally supports only Point, LineString and Polygon and preserves only scalar properties. There is no CRS transformation, basemap request or claim of survey-grade geodesy. See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) and [docs/PRIVACY-SECURITY.md](docs/PRIVACY-SECURITY.md). +Geometry collections are limited to 20,000 geometry nodes, 16 nesting levels +and 200,000 coordinates. GPX/CSV conversions flatten geometry grouping and +report that loss. Scalar properties are preserved; there is no CRS +transformation, basemap request or claim of survey-grade geodesy. See +[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) and [docs/PRIVACY-SECURITY.md](docs/PRIVACY-SECURITY.md). ## Development @@ -26,7 +32,7 @@ npm run test:browser ## Release -`npm run release:artifact` creates a deterministic `release/geo-tools-0.1.0.zip` and checksum sidecar. +`npm run release:artifact` creates a deterministic `release/geo-tools-0.2.0.zip` and checksum sidecar. ## Licence diff --git a/SOURCE.md b/SOURCE.md index 9c01523..b21dcb0 100644 --- a/SOURCE.md +++ b/SOURCE.md @@ -1,7 +1,7 @@ # Corresponding source -The corresponding source for Geo Tools 0.1.0 is available at: +The corresponding source for Geo Tools 0.2.0 is available at: -https://git.add-ideas.de/lotobo/geo-tools/src/tag/v0.1.0 +https://git.add-ideas.de/lotobo/geo-tools/src/tag/v0.2.0 Build with Node.js 22, npm 11, `npm ci`, and `npm run release:artifact`. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 9f968db..ccdafdd 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -4,9 +4,9 @@ Geo Tools 0.1.0 directly depends on these runtime packages: | Package | Pinned version | Declared licence | | -------------------------------- | -------------: | ---------------- | -| `@add-ideas/toolbox-contract` | 0.2.3 | Apache-2.0 | -| `@add-ideas/toolbox-helpers` | 0.1.0 | GPL-3.0-or-later | -| `@add-ideas/toolbox-shell-react` | 0.2.3 | Apache-2.0 | +| `@add-ideas/toolbox-contract` | 0.3.0 | Apache-2.0 | +| `@add-ideas/toolbox-helpers` | 0.2.0 | GPL-3.0-or-later | +| `@add-ideas/toolbox-shell-react` | 0.3.0 | Apache-2.0 | | `react` | 19.2.8 | MIT | | `react-dom` | 19.2.8 | MIT | diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index e75f583..2b0163c 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,7 +1,9 @@ # Architecture -Geo Tools is a static React/Vite application wrapped in the shared Toolbox shell. `geo/formats.ts` parses GeoJSON, GPX, KML or a narrow coordinate-CSV dialect into a project-owned `FeatureCollection` model containing only Point, LineString and Polygon geometries plus scalar properties. Conversion serialises that model and returns visible loss notes. +Geo Tools is a static React/Vite application wrapped in the shared Toolbox shell. `geo/formats.ts` parses GeoJSON, GPX, KML or a narrow coordinate-CSV dialect into a project-owned `FeatureCollection` model covering Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon and recursively bounded GeometryCollection values plus scalar properties. Conversion serialises that model and returns visible loss notes where a target format must flatten structure. -`geo/model.ts` validates WGS 84 coordinate ranges and enforces the collection-wide coordinate limit. `geo/analysis.ts` computes bounds, haversine line distance and elevation changes, and performs iterative Douglas–Peucker simplification. The local sketch projects coordinates only into its own SVG viewport; it loads no basemap, tiles or projection service. +`geo/model.ts` validates WGS 84 coordinate ranges and enforces collection-wide coordinate, geometry-count and nesting-depth limits. `geo/analysis.ts` computes bounds, haversine line distance and elevation changes, and performs iterative Douglas–Peucker simplification across relevant nested path members. The local sketch projects every supported geometry path only into its own SVG viewport; it loads no basemap, tiles or projection service. + +`geo/editor.ts` assigns stable feature/geometry/coordinate paths across the complete geometry union, performs immutable point or feature replacement, and sends every result back through the common validator before returning it. Track summaries recurse through GeometryCollections, and direction reversal is limited to line geometries. The local ruler returns a spherical distance, initial bearing and coordinate midpoint; it does not imply a routing path or projected measurement. XML input is lexically bounded before DOM construction and rejects DOCTYPE, entity and stylesheet declarations. Version 0.1 uses no worker, storage or server API. Relative entry and asset URLs keep the build relocatable below a nested portal path. diff --git a/docs/PRIVACY-SECURITY.md b/docs/PRIVACY-SECURITY.md index 7d8440e..8baaf05 100644 --- a/docs/PRIVACY-SECURITY.md +++ b/docs/PRIVACY-SECURITY.md @@ -2,6 +2,8 @@ Imported coordinates and generated exports stay in page memory. The app has no telemetry, analytics, account, persistence, map-tile request or other runtime network path. Names and properties are rendered as React text and escaped on XML export. -Text/file input is limited to 16 MiB and the common model to 200,000 coordinates. XML is rejected above 250,000 elements or 256 levels before DOM parsing; DOCTYPE, entity and XML stylesheet declarations are rejected. These controls bound supported input but are not a general XML sanitiser. +Text/file input is limited to 16 MiB and the common model to 200,000 coordinates, 20,000 geometries and 16 GeometryCollection levels. XML is rejected above 250,000 elements or 256 levels before DOM parsing; DOCTYPE, entity and XML stylesheet declarations are rejected. These controls bound supported input but are not a general XML sanitiser. -Coordinates are interpreted as WGS 84 longitude/latitude. There is no CRS transformation and the local SVG sketch is not a map projection. Distance is a spherical approximation, elevation gain/loss uses the supplied samples without smoothing, and conversion intentionally drops unsupported styles, extensions, geometry structure or properties as stated beside each result. Do not treat results as survey-grade measurements. +Coordinates are interpreted as WGS 84 longitude/latitude. There is no CRS transformation and the local SVG sketch is not a map projection. Distance is a spherical approximation, elevation gain/loss uses the supplied samples without smoothing, and conversion intentionally drops unsupported styles, extensions, properties, or flattens geometry structure where the target format cannot preserve it, as stated beside each result. Do not treat results as survey-grade measurements. + +Geometry editing validates coordinate bounds, model size and recursion limits but does not repair topology, ring winding, self-intersections or semantic feature relationships. GPX import retains track geometry and available elevations in the common model; timestamps, extensions and vendor-specific track metadata are not round-tripped. The two-point ruler measures a great-circle segment only and never contacts a routing or map service. diff --git a/package-lock.json b/package-lock.json index 9e49d89..960e91b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,22 +1,22 @@ { "name": "geo-tools", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "geo-tools", - "version": "0.1.0", + "version": "0.2.0", "license": "GPL-3.0-or-later", "dependencies": { - "@add-ideas/toolbox-contract": "0.2.3", - "@add-ideas/toolbox-helpers": "0.1.0", - "@add-ideas/toolbox-shell-react": "0.2.3", + "@add-ideas/toolbox-contract": "0.3.0", + "@add-ideas/toolbox-helpers": "0.2.0", + "@add-ideas/toolbox-shell-react": "0.3.0", "react": "19.2.8", "react-dom": "19.2.8" }, "devDependencies": { - "@add-ideas/toolbox-testkit": "0.2.3", + "@add-ideas/toolbox-testkit": "0.3.0", "@eslint/js": "10.0.1", "@playwright/test": "1.62.1", "@testing-library/jest-dom": "6.9.1", @@ -42,24 +42,24 @@ } }, "node_modules/@add-ideas/toolbox-contract": { - "version": "0.2.3", - "license": "Apache-2.0", - "engines": { - "node": ">=20" - } + "version": "0.3.0", + "resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-contract/-/0.3.0/toolbox-contract-0.3.0.tgz", + "integrity": "sha512-dKrK7BjOFwqJaBfJuhKxZKIld4sH0AKjEn6a0yLnbdMUFY+fFv4VSLGV2tNSBD016gumc2iNqOjUj/ld7x4rtA==", + "license": "Apache-2.0" }, "node_modules/@add-ideas/toolbox-helpers": { - "version": "0.1.0", - "license": "GPL-3.0-or-later", - "engines": { - "node": ">=22" - } + "version": "0.2.0", + "resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-helpers/-/0.2.0/toolbox-helpers-0.2.0.tgz", + "integrity": "sha512-SdOqkw+P+3J3fa5iVkzb5P15rVepB001GNV21Oh8w0CZcVL+YRltgD/s+MVcTyrNijWQf3E5vtQON/3N2LLyKg==", + "license": "GPL-3.0-or-later" }, "node_modules/@add-ideas/toolbox-shell-react": { - "version": "0.2.3", + "version": "0.3.0", + "resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-shell-react/-/0.3.0/toolbox-shell-react-0.3.0.tgz", + "integrity": "sha512-74p6JzAOG0YCAKdlc1hLofV4ZIko7vb448S75cIiM88PKm93EHl5VD7g8YVyfM56Ui97UY9dmy+Whiq4sGzpsg==", "license": "Apache-2.0", "dependencies": { - "@add-ideas/toolbox-contract": "0.2.3" + "@add-ideas/toolbox-contract": "0.3.0" }, "peerDependencies": { "react": ">=18 <20", @@ -67,17 +67,16 @@ } }, "node_modules/@add-ideas/toolbox-testkit": { - "version": "0.2.3", + "version": "0.3.0", + "resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-testkit/-/0.3.0/toolbox-testkit-0.3.0.tgz", + "integrity": "sha512-4Fk+oSvZFspOMIXr8Xy040nhAaBsIQAzsGyXWSpjn3+k3yBKq7nB1r5zCHhsXzfdLzvPDAx2KcmSNOhM330D9w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@add-ideas/toolbox-contract": "0.2.3" + "@add-ideas/toolbox-contract": "0.3.0" }, "bin": { "toolbox-check": "dist/cli.js" - }, - "engines": { - "node": ">=20" } }, "node_modules/@adobe/css-tools": { diff --git a/package.json b/package.json index 5836651..8208540 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "geo-tools", - "version": "0.1.0", + "version": "0.2.0", "description": "Inspect, convert and analyse geospatial files locally in the browser.", "license": "GPL-3.0-or-later", "author": "Albrecht Degering", @@ -39,14 +39,14 @@ "release:artifact": "npm run check && npm run test:browser && npm run package:release -- --force" }, "dependencies": { - "@add-ideas/toolbox-contract": "0.2.3", - "@add-ideas/toolbox-helpers": "0.1.0", - "@add-ideas/toolbox-shell-react": "0.2.3", + "@add-ideas/toolbox-contract": "0.3.0", + "@add-ideas/toolbox-helpers": "0.2.0", + "@add-ideas/toolbox-shell-react": "0.3.0", "react": "19.2.8", "react-dom": "19.2.8" }, "devDependencies": { - "@add-ideas/toolbox-testkit": "0.2.3", + "@add-ideas/toolbox-testkit": "0.3.0", "@eslint/js": "10.0.1", "@playwright/test": "1.62.1", "@testing-library/jest-dom": "6.9.1", diff --git a/playwright.config.ts b/playwright.config.ts index 731da62..2ab6a22 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -15,7 +15,25 @@ export default defineConfig({ timeout: 180_000, }, projects: [ - { name: "chromium", use: { ...devices["Desktop Chrome"] } }, - { name: "firefox", use: { ...devices["Desktop Firefox"] } }, + { + name: "chromium", + testIgnore: /responsive\.spec\.ts/, + use: { ...devices["Desktop Chrome"] }, + }, + { + name: "firefox", + testIgnore: /responsive\.spec\.ts/, + use: { ...devices["Desktop Firefox"] }, + }, + { + name: "webkit", + testIgnore: /responsive\.spec\.ts/, + use: { ...devices["Desktop Safari"] }, + }, + { + name: "mobile-chromium", + testMatch: /responsive\.spec\.ts/, + use: { ...devices["Pixel 5"] }, + }, ], }); diff --git a/public/CHANGELOG.md b/public/CHANGELOG.md index cc16c8e..0fc2f59 100644 --- a/public/CHANGELOG.md +++ b/public/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.2.0 - 2026-09-02 + +- Added validated full-GeoJSON feature editing, append/delete, nested coordinate addressing, line-track reversal, and GPX-derived track inventories. +- Added a local great-circle distance/bearing/midpoint ruler and Toolbox format/capability metadata while retaining the tile-free, WGS 84-only boundary. + ## 0.1.0 - 2026-09-01 - Added the initial local-first Geo Tools workbench. diff --git a/public/LICENSES/npm-runtime-licenses.txt b/public/LICENSES/npm-runtime-licenses.txt index 45a7c95..4f412f1 100644 --- a/public/LICENSES/npm-runtime-licenses.txt +++ b/public/LICENSES/npm-runtime-licenses.txt @@ -1,5 +1,5 @@ ============================================================================== -@add-ideas/toolbox-contract@0.2.3 +@add-ideas/toolbox-contract@0.3.0 Declared licence: Apache-2.0 ============================================================================== --- LICENSE --- @@ -198,7 +198,7 @@ Declared licence: Apache-2.0 ============================================================================== -@add-ideas/toolbox-helpers@0.1.0 +@add-ideas/toolbox-helpers@0.2.0 Declared licence: GPL-3.0-or-later ============================================================================== --- LICENSE --- @@ -879,7 +879,7 @@ Public License instead of this License. But first, please read ============================================================================== -@add-ideas/toolbox-shell-react@0.2.3 +@add-ideas/toolbox-shell-react@0.3.0 Declared licence: Apache-2.0 ============================================================================== --- LICENSE --- diff --git a/public/README.md b/public/README.md index d3f1ae6..26ac563 100644 --- a/public/README.md +++ b/public/README.md @@ -1,18 +1,24 @@ # Geo Tools -Inspect, convert and analyse geospatial files locally in the browser. +Inspect, edit, measure and convert geospatial files locally in the browser. Geo Tools is a standalone local-first application in the [add·ideas Toolbox](https://git.add-ideas.de/lotobo/toolbox-portal). Inputs are processed in the browser and are not uploaded. ## Version 0.1 scope -- Bounded GeoJSON, GPX, KML and coordinate-CSV parsing into a small common model +- Bounded GeoJSON, GPX, KML and coordinate-CSV parsing into a common model with Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon and nested GeometryCollection support - WGS 84 bounds, point counts, great-circle LineString distance and elevation gain/loss - Explicit lossy conversion among the four supported formats - Iterative Douglas–Peucker LineString simplification and decimal-degree/DMS conversion - A coordinate-only local sketch with no map tiles or projection service +- Full-model GeoJSON geometry replacement, append/delete, nested coordinate editing and line-direction reversal with validation after every edit +- GPX-derived track inventory plus a local two-point distance, initial-bearing and midpoint ruler -The common model intentionally supports only Point, LineString and Polygon and preserves only scalar properties. There is no CRS transformation, basemap request or claim of survey-grade geodesy. See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) and [docs/PRIVACY-SECURITY.md](docs/PRIVACY-SECURITY.md). +Geometry collections are limited to 20,000 geometry nodes, 16 nesting levels +and 200,000 coordinates. GPX/CSV conversions flatten geometry grouping and +report that loss. Scalar properties are preserved; there is no CRS +transformation, basemap request or claim of survey-grade geodesy. See +[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) and [docs/PRIVACY-SECURITY.md](docs/PRIVACY-SECURITY.md). ## Development @@ -26,7 +32,7 @@ npm run test:browser ## Release -`npm run release:artifact` creates a deterministic `release/geo-tools-0.1.0.zip` and checksum sidecar. +`npm run release:artifact` creates a deterministic `release/geo-tools-0.2.0.zip` and checksum sidecar. ## Licence diff --git a/public/SOURCE.md b/public/SOURCE.md index 9c01523..b21dcb0 100644 --- a/public/SOURCE.md +++ b/public/SOURCE.md @@ -1,7 +1,7 @@ # Corresponding source -The corresponding source for Geo Tools 0.1.0 is available at: +The corresponding source for Geo Tools 0.2.0 is available at: -https://git.add-ideas.de/lotobo/geo-tools/src/tag/v0.1.0 +https://git.add-ideas.de/lotobo/geo-tools/src/tag/v0.2.0 Build with Node.js 22, npm 11, `npm ci`, and `npm run release:artifact`. diff --git a/public/THIRD_PARTY_NOTICES.md b/public/THIRD_PARTY_NOTICES.md index 9f968db..ccdafdd 100644 --- a/public/THIRD_PARTY_NOTICES.md +++ b/public/THIRD_PARTY_NOTICES.md @@ -4,9 +4,9 @@ Geo Tools 0.1.0 directly depends on these runtime packages: | Package | Pinned version | Declared licence | | -------------------------------- | -------------: | ---------------- | -| `@add-ideas/toolbox-contract` | 0.2.3 | Apache-2.0 | -| `@add-ideas/toolbox-helpers` | 0.1.0 | GPL-3.0-or-later | -| `@add-ideas/toolbox-shell-react` | 0.2.3 | Apache-2.0 | +| `@add-ideas/toolbox-contract` | 0.3.0 | Apache-2.0 | +| `@add-ideas/toolbox-helpers` | 0.2.0 | GPL-3.0-or-later | +| `@add-ideas/toolbox-shell-react` | 0.3.0 | Apache-2.0 | | `react` | 19.2.8 | MIT | | `react-dom` | 19.2.8 | MIT | diff --git a/public/docs/ARCHITECTURE.md b/public/docs/ARCHITECTURE.md index e75f583..2b0163c 100644 --- a/public/docs/ARCHITECTURE.md +++ b/public/docs/ARCHITECTURE.md @@ -1,7 +1,9 @@ # Architecture -Geo Tools is a static React/Vite application wrapped in the shared Toolbox shell. `geo/formats.ts` parses GeoJSON, GPX, KML or a narrow coordinate-CSV dialect into a project-owned `FeatureCollection` model containing only Point, LineString and Polygon geometries plus scalar properties. Conversion serialises that model and returns visible loss notes. +Geo Tools is a static React/Vite application wrapped in the shared Toolbox shell. `geo/formats.ts` parses GeoJSON, GPX, KML or a narrow coordinate-CSV dialect into a project-owned `FeatureCollection` model covering Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon and recursively bounded GeometryCollection values plus scalar properties. Conversion serialises that model and returns visible loss notes where a target format must flatten structure. -`geo/model.ts` validates WGS 84 coordinate ranges and enforces the collection-wide coordinate limit. `geo/analysis.ts` computes bounds, haversine line distance and elevation changes, and performs iterative Douglas–Peucker simplification. The local sketch projects coordinates only into its own SVG viewport; it loads no basemap, tiles or projection service. +`geo/model.ts` validates WGS 84 coordinate ranges and enforces collection-wide coordinate, geometry-count and nesting-depth limits. `geo/analysis.ts` computes bounds, haversine line distance and elevation changes, and performs iterative Douglas–Peucker simplification across relevant nested path members. The local sketch projects every supported geometry path only into its own SVG viewport; it loads no basemap, tiles or projection service. + +`geo/editor.ts` assigns stable feature/geometry/coordinate paths across the complete geometry union, performs immutable point or feature replacement, and sends every result back through the common validator before returning it. Track summaries recurse through GeometryCollections, and direction reversal is limited to line geometries. The local ruler returns a spherical distance, initial bearing and coordinate midpoint; it does not imply a routing path or projected measurement. XML input is lexically bounded before DOM construction and rejects DOCTYPE, entity and stylesheet declarations. Version 0.1 uses no worker, storage or server API. Relative entry and asset URLs keep the build relocatable below a nested portal path. diff --git a/public/docs/PRIVACY-SECURITY.md b/public/docs/PRIVACY-SECURITY.md index 7d8440e..8baaf05 100644 --- a/public/docs/PRIVACY-SECURITY.md +++ b/public/docs/PRIVACY-SECURITY.md @@ -2,6 +2,8 @@ Imported coordinates and generated exports stay in page memory. The app has no telemetry, analytics, account, persistence, map-tile request or other runtime network path. Names and properties are rendered as React text and escaped on XML export. -Text/file input is limited to 16 MiB and the common model to 200,000 coordinates. XML is rejected above 250,000 elements or 256 levels before DOM parsing; DOCTYPE, entity and XML stylesheet declarations are rejected. These controls bound supported input but are not a general XML sanitiser. +Text/file input is limited to 16 MiB and the common model to 200,000 coordinates, 20,000 geometries and 16 GeometryCollection levels. XML is rejected above 250,000 elements or 256 levels before DOM parsing; DOCTYPE, entity and XML stylesheet declarations are rejected. These controls bound supported input but are not a general XML sanitiser. -Coordinates are interpreted as WGS 84 longitude/latitude. There is no CRS transformation and the local SVG sketch is not a map projection. Distance is a spherical approximation, elevation gain/loss uses the supplied samples without smoothing, and conversion intentionally drops unsupported styles, extensions, geometry structure or properties as stated beside each result. Do not treat results as survey-grade measurements. +Coordinates are interpreted as WGS 84 longitude/latitude. There is no CRS transformation and the local SVG sketch is not a map projection. Distance is a spherical approximation, elevation gain/loss uses the supplied samples without smoothing, and conversion intentionally drops unsupported styles, extensions, properties, or flattens geometry structure where the target format cannot preserve it, as stated beside each result. Do not treat results as survey-grade measurements. + +Geometry editing validates coordinate bounds, model size and recursion limits but does not repair topology, ring winding, self-intersections or semantic feature relationships. GPX import retains track geometry and available elevations in the common model; timestamps, extensions and vendor-specific track metadata are not round-tripped. The two-point ruler measures a great-circle segment only and never contacts a routing or map service. diff --git a/public/sw.js b/public/sw.js index a414f2b..bcb4cde 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,5 +1,5 @@ const CACHE_PREFIX = "geo-tools-shell-"; -const CACHE_NAME = CACHE_PREFIX + "0.1.0"; +const CACHE_NAME = CACHE_PREFIX + "0.2.0"; const CORE = ["./", "./manifest.webmanifest", "./favicon.svg"]; self.addEventListener("install", (event) => { event.waitUntil( diff --git a/public/toolbox-app.json b/public/toolbox-app.json index c2cef2f..25b44f7 100644 --- a/public/toolbox-app.json +++ b/public/toolbox-app.json @@ -3,12 +3,20 @@ "schemaVersion": 1, "id": "de.add-ideas.geo-tools", "name": "Geo Tools", - "version": "0.1.0", - "description": "Inspect, convert and analyse geospatial files locally in the browser.", + "version": "0.2.0", + "description": "Inspect, edit, measure and convert geospatial files locally.", "entry": "./", "icon": "./favicon.svg", "categories": ["geography", "data", "files"], - "tags": ["geojson", "gpx", "kml", "track", "coordinates"], + "tags": [ + "geojson", + "gpx", + "kml", + "track", + "geometry", + "measure", + "coordinates" + ], "integration": { "contextVersion": 1, "launchModes": ["navigate", "new-tab"], @@ -21,6 +29,53 @@ "crossOriginIsolated": false, "topLevelContext": false }, + "io": { + "accepts": [ + { + "mediaType": "application/geo+json", + "extensions": [".geojson", ".json"], + "label": "GeoJSON" + }, + { + "mediaType": "application/gpx+xml", + "extensions": [".gpx"], + "label": "GPX" + }, + { + "mediaType": "application/vnd.google-earth.kml+xml", + "extensions": [".kml"], + "label": "KML" + }, + { + "mediaType": "text/csv", + "extensions": [".csv"], + "label": "Coordinate CSV" + } + ], + "produces": [ + { + "mediaType": "application/geo+json", + "extensions": [".geojson", ".json"], + "label": "GeoJSON" + }, + { + "mediaType": "application/gpx+xml", + "extensions": [".gpx"], + "label": "GPX" + }, + { + "mediaType": "application/vnd.google-earth.kml+xml", + "extensions": [".kml"], + "label": "KML" + }, + { + "mediaType": "text/csv", + "extensions": [".csv"], + "label": "Coordinate CSV" + } + ] + }, + "capabilities": { "required": [], "optional": ["clipboard-write"] }, "privacy": { "processing": "local", "fileUploads": true, diff --git a/src/components/Workbench.tsx b/src/components/Workbench.tsx index 20c0f13..e4651a6 100644 --- a/src/components/Workbench.tsx +++ b/src/components/Workbench.tsx @@ -1,9 +1,19 @@ import { useMemo, useState } from "react"; import { triggerBlobDownload } from "@add-ideas/toolbox-helpers"; import { analyseCollection, simplifyCollection, toDms } from "../geo/analysis"; +import { + appendFeature, + listPositionReferences, + measureSegment, + removeFeature, + replaceFeature, + reverseFeatureTracks, + summarizeTracks, +} from "../geo/editor"; import { detectAndParse, serializeGeo } from "../geo/formats"; import { collectionPositions, + geometryPaths, type GeoCollection, type GeoParseResult, type Position, @@ -22,18 +32,22 @@ const example = `{ type Format = GeoParseResult["sourceFormat"]; +function featureJson(result: GeoParseResult, index: number): string { + const feature = result.collection.features[index]; + return feature ? JSON.stringify(feature, null, 2) : ""; +} + function pointsForGeometry( collection: GeoCollection, ): { points: Position[]; closed: boolean }[] { const paths: { points: Position[]; closed: boolean }[] = []; for (const feature of collection.features) { - if (feature.geometry.type === "Point") - paths.push({ points: [feature.geometry.coordinates], closed: false }); - else if (feature.geometry.type === "LineString") - paths.push({ points: feature.geometry.coordinates, closed: false }); - else - for (const points of feature.geometry.coordinates) - paths.push({ points, closed: true }); + paths.push( + ...geometryPaths(feature.geometry).map(({ points, closed }) => ({ + points, + closed, + })), + ); } return paths; } @@ -99,6 +113,12 @@ export function Workbench() { const [tolerance, setTolerance] = useState(10); const [latitude, setLatitude] = useState(52.52); const [longitude, setLongitude] = useState(13.405); + const [featureIndex, setFeatureIndex] = useState(0); + const [featureDraft, setFeatureDraft] = useState(() => + featureJson(detectAndParse(example, "auto"), 0), + ); + const [measureStart, setMeasureStart] = useState("13.3777,52.5163"); + const [measureEnd, setMeasureEnd] = useState("13.4050,52.5200"); const statistics = useMemo( () => analyseCollection(parsed.collection), [parsed], @@ -107,6 +127,21 @@ export function Workbench() { () => serializeGeo(parsed.collection, target), [parsed, target], ); + const tracks = useMemo(() => summarizeTracks(parsed.collection), [parsed]); + const positionCount = useMemo( + () => listPositionReferences(parsed.collection).length, + [parsed], + ); + const measurement = useMemo(() => { + try { + return measureSegment( + measureStart.split(",").map(Number), + measureEnd.split(",").map(Number), + ); + } catch { + return undefined; + } + }, [measureEnd, measureStart]); const coordinateOutput = useMemo(() => { try { return [toDms(latitude, "latitude"), toDms(longitude, "longitude")]; @@ -117,7 +152,10 @@ export function Workbench() { const parse = () => { try { - setParsed(detectAndParse(source, sourceFormat)); + const next = detectAndParse(source, sourceFormat); + setParsed(next); + setFeatureIndex(0); + setFeatureDraft(featureJson(next, 0)); setError(""); } catch (reason) { setError( @@ -146,7 +184,10 @@ export function Workbench() { : "auto"; setSourceFormat(format); try { - setParsed(detectAndParse(text, format)); + const next = detectAndParse(text, format); + setParsed(next); + setFeatureIndex(0); + setFeatureDraft(featureJson(next, 0)); setError(""); } catch (reason) { setError( @@ -156,14 +197,16 @@ export function Workbench() { }; const simplify = () => { try { - setParsed((current) => ({ - ...current, - collection: simplifyCollection(current.collection, tolerance), + const next = { + ...parsed, + collection: simplifyCollection(parsed.collection, tolerance), warnings: [ - ...current.warnings, + ...parsed.warnings, `LineStrings simplified with a ${tolerance} m equirectangular Douglas–Peucker tolerance.`, ], - })); + }; + setParsed(next); + setFeatureDraft(featureJson(next, featureIndex)); setError(""); } catch (reason) { setError( @@ -171,6 +214,33 @@ export function Workbench() { ); } }; + const editFeature = (action: "replace" | "append") => { + try { + const value = JSON.parse(featureDraft) as unknown; + const collection = + action === "replace" + ? replaceFeature(parsed.collection, featureIndex, value) + : appendFeature(parsed.collection, value); + const next = { + ...parsed, + collection: collection, + warnings: [ + ...parsed.warnings, + `Feature ${action === "replace" ? "replaced" : "appended"} in the local editable model.`, + ], + }; + const nextIndex = + action === "append" ? collection.features.length - 1 : featureIndex; + setParsed(next); + setFeatureIndex(nextIndex); + setFeatureDraft(featureJson(next, nextIndex)); + setError(""); + } catch (reason) { + setError( + reason instanceof Error ? reason.message : "Feature edit failed.", + ); + } + }; return (
@@ -274,6 +344,171 @@ export function Workbench() { ))} +
+
+
+

Geometry editor

+

Edit any GeoJSON feature

+

+ The draft accepts the complete Point, MultiPoint, LineString, + MultiLineString, Polygon, MultiPolygon or GeometryCollection + model. Every replacement is re-parsed and bounded before it + becomes current. +

+
+ +