diff --git a/.forgejo/workflows/update_community_index.yml b/.forgejo/workflows/update_community_index.yml new file mode 100644 index 000000000..77b0e8f8c --- /dev/null +++ b/.forgejo/workflows/update_community_index.yml @@ -0,0 +1,31 @@ +name: Weekly data updates +on: + schedule: + -cron: "* * * * 1" + +jobs: + deploy_on_hetzner_single: + runs-on: [ ubuntu-latest, hetzner-access ] + + runs-on: [ ubuntu-latest, hetzner-access ] + steps: + - uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: package-lock.json + + - name: install deps + run: npm ci + shell: bash + + - name: update community index files + shell: bash + run: | + mkdir community-index + npm run download:community-index -- community-index/ + zip community-index.zip community-index/* + scp community-index.zip hetzner:data/ diff --git a/package.json b/package.json index 4ff48086b..5e868596a 100644 --- a/package.json +++ b/package.json @@ -119,6 +119,7 @@ "download:editor-layer-index": "vite-node scripts/downloadEli.ts", "download:stats": "vite-node scripts/GenerateSeries.ts", "download:images": "vite-node scripts/generateImageAnalysis.ts -- ~/data/imgur-image-backup/", + "download:community-index": "vite-node scripts/downloadCommunityIndex.ts -- /tmp/test", "weblate:add-upstream": "git remote add weblate https://translate.mapcomplete.org/git/mapcomplete/core/ ; git remote update weblate", "weblate:fix": "npm run weblate:add-upstream && git merge weblate/master && git rebase origin/master && git push origin master", "lint": "npm run lint:prettier && npm run lint:eslint && npm run lint:themes", diff --git a/scripts/downloadCommunityIndex.ts b/scripts/downloadCommunityIndex.ts new file mode 100644 index 000000000..fe4d4dcc0 --- /dev/null +++ b/scripts/downloadCommunityIndex.ts @@ -0,0 +1,116 @@ +import Script from "./Script" +import { CommunityResource } from "../src/Logic/Web/CommunityIndex" +import { Utils } from "../src/Utils" +import { FeatureCollection, MultiPolygon, Polygon } from "geojson" +import { writeFileSync } from "fs" +import { GeoOperations } from "../src/Logic/GeoOperations" +import { Tiles } from "../src/Models/TileRange" +import ScriptUtils from "./ScriptUtils" + +class DownloadCommunityIndex extends Script { + + constructor() { + super("Updates the community index") + } + + printHelp() { + console.log("Arguments are:\noutputdirectory") + } + + private static targetZoomlevel: number = 6 + private static upstreamUrl: string = "https://raw.githubusercontent.com/osmlab/osm-community-index/main/dist/" + + /** + * Prunes away unnecessary fields from a CommunityResource + * @private + */ + private static stripResource(r: Readonly): CommunityResource { + return { + id: r.id, + languageCodes: r.languageCodes, + account: r.account, + type: r.type, + resolved: { + name: r.resolved.name, + description: r.resolved.description, + url: r.resolved.url + } + } + } + + private static stripResourcesObj(resources: Readonly>>) { + const stripped: Record = {} + for (const k in resources) { + stripped[k] = DownloadCommunityIndex.stripResource(resources[k]) + } + return stripped + } + + public static async update(targetDirectory: string) { + const data = await Utils.downloadJson, + nameEn: string, + id: string + }>>(DownloadCommunityIndex.upstreamUrl + "completeFeatureCollection.json" + ) + const features = data.features + const global = features.find( + f => f.id === "Q2" + ) + const globalProperties = DownloadCommunityIndex.stripResourcesObj(global.properties.resources) + writeFileSync(targetDirectory + "/global.json", JSON.stringify(globalProperties), "utf8") + console.log("Written global properties") + + const types = new Set() + for (const f of features) { + const res = f.properties.resources + for (const k in res) { + types.add(res[k].type) + } + } + for (const type of types) { + const url = `${DownloadCommunityIndex.upstreamUrl}img/${type}.svg` + await ScriptUtils.DownloadFileTo(url, `${targetDirectory}/${type}.svg`) + } + const local = features.filter(f => f.id !== "Q2") + const spread = GeoOperations.spreadIntoBboxes(local, DownloadCommunityIndex.targetZoomlevel) + let written = 0 + let skipped = 0 + writeFileSync(targetDirectory + "local.geojson", JSON.stringify({ type: "FeatureCollection", features: local })) + for (const tileIndex of spread.keys()) { + const features = spread.get(tileIndex) + const clipped = GeoOperations.clipAllInBox(features, tileIndex) + if (clipped.length === 0) { + skipped++ + features.push(Tiles.asGeojson(tileIndex)) + writeFileSync(`${targetDirectory + tileIndex}_skipped.geojson`, JSON.stringify({ + type: "FeatureCollection", features + })) + continue + } + const [z, x, y] = Tiles.tile_from_index(tileIndex) + const path = `${targetDirectory}/tile_${z}_${x}_${y}.geojson` + clipped.forEach((f) => { + delete f.bbox + }) + writeFileSync(path, JSON.stringify({ type: "FeatureCollection", features: clipped }), "utf8") + written++ + console.log(`Written tile ${path}`) + } + console.log(`Created ${written} tiles, skipped ${skipped}`) + } + + + async main(args: string[]): Promise { + const path = args[0] + if (!path) { + this.printHelp() + return + } + + await DownloadCommunityIndex.update(path) + + } +} + +new DownloadCommunityIndex().run() diff --git a/scripts/slice.ts b/scripts/slice.ts index 77b7b65c9..a4a17831a 100644 --- a/scripts/slice.ts +++ b/scripts/slice.ts @@ -1,13 +1,12 @@ import * as fs from "fs" -import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource" import * as readline from "readline" import ScriptUtils from "./ScriptUtils" import { Utils } from "../Utils" import Script from "./Script" -import { BBox } from "../Logic/BBox" import { GeoOperations } from "../Logic/GeoOperations" import { Tiles } from "../Models/TileRange" import { Feature } from "geojson" +import { features } from "monaco-editor/esm/metadata" /** * This script slices a big newline-delimeted geojson file into tiled geojson @@ -96,34 +95,15 @@ class Slice extends Script { features: Feature[], tileIndex: number, outputDirectory: string, - doSlice: boolean, + doClip: boolean, handled: number, maxNumberOfTiles: number - ) { + ): boolean { + if (doClip) { + features = GeoOperations.clipAllInBox(features, tileIndex) + } const [z, x, y] = Tiles.tile_from_index(tileIndex) const path = `${outputDirectory}/tile_${z}_${x}_${y}.geojson` - const box = BBox.fromTileIndex(tileIndex) - if (doSlice) { - features = Utils.NoNull( - features.map((f) => { - const bbox = box.asGeoJson({}) - const properties = { - ...f.properties, - id: (f.properties?.id ?? "") + "_" + z + "_" + x + "_" + y, - } - - if (GeoOperations.completelyWithin(bbox, f)) { - bbox.properties = properties - return bbox - } - const intersection = GeoOperations.intersect(f, box.asGeoJson({})) - if (intersection) { - intersection.properties = properties - } - return intersection - }) - ) - } features.forEach((f) => { delete f.bbox }) @@ -177,7 +157,7 @@ class Slice extends Script { } console.log("Using directory ", outputDirectory) - let allFeatures: any[] + let allFeatures: Feature[] if (inputFile.endsWith(".geojson")) { console.log("Detected geojson") allFeatures = await this.readFeaturesFromGeoJson(inputFile) @@ -202,18 +182,16 @@ class Slice extends Script { } const maxNumberOfTiles = Math.pow(2, zoomlevel) * Math.pow(2, zoomlevel) let handled = 0 - StaticFeatureSource.fromGeojson(allFeatures).features.addCallbackAndRun((feats) => { - GeoOperations.slice(zoomlevel, feats).forEach((tileData, tileIndex) => { - handled = handled + 1 - this.handleTileData( - tileData, - tileIndex, - outputDirectory, - doSlice, - handled, - maxNumberOfTiles - ) - }) + GeoOperations.slice(zoomlevel, features).forEach((tileData, tileIndex) => { + handled = handled + 1 + this.handleTileData( + tileData, + tileIndex, + outputDirectory, + doSlice, + handled, + maxNumberOfTiles + ) }) } } diff --git a/src/Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage.ts b/src/Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage.ts index 6e2510b72..59cf8b51b 100644 --- a/src/Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage.ts +++ b/src/Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage.ts @@ -75,7 +75,7 @@ export default class SaveFeatureSourceToLocalStorage { this.storage = storage const singleTileSavers: Map = new Map() features.features.addCallbackAndRunD((features) => { - const sliced = GeoOperations.slice(zoomlevel, features) + const sliced = GeoOperations.spreadIntoBboxes(features, zoomlevel) sliced.forEach((features, tileIndex) => { let tileSaver = singleTileSavers.get(tileIndex) diff --git a/src/Logic/GeoOperations.ts b/src/Logic/GeoOperations.ts index 63fe2ccb7..a6faf3d8b 100644 --- a/src/Logic/GeoOperations.ts +++ b/src/Logic/GeoOperations.ts @@ -1,6 +1,6 @@ import { BBox } from "./BBox" import * as turf from "@turf/turf" -import { AllGeoJSON, booleanWithin, Coord } from "@turf/turf" +import { AllGeoJSON, booleanWithin, Coord, Polygon } from "@turf/turf" import { Feature, FeatureCollection, @@ -9,13 +9,13 @@ import { MultiLineString, MultiPolygon, Point, - Polygon, - Position, + Position } from "geojson" import { Tiles } from "../Models/TileRange" import { Utils } from "../Utils" import { NearestPointOnLine } from "@turf/nearest-point-on-line" -;("use strict") + +("use strict") export class GeoOperations { private static readonly _earthRadius = 6378137 @@ -29,7 +29,7 @@ export class GeoOperations { "behind", "sharp_left", "left", - "slight_left", + "slight_left" ] as const private static reverseBearing = { N: 0, @@ -47,7 +47,7 @@ export class GeoOperations { W: 270, WNW: 292.5, NW: 315, - NNW: 337.5, + NNW: 337.5 } /** @@ -61,8 +61,8 @@ export class GeoOperations { } public static intersect( - f0: Feature, - f1: Feature + f0: Readonly>, + f1: Readonly> ): Feature | null { return turf.intersect(f0, f1) } @@ -309,7 +309,7 @@ export class GeoOperations { bufferSizeInMeter: number ): Feature | FeatureCollection { return turf.buffer(feature, bufferSizeInMeter / 1000, { - units: "kilometers", + units: "kilometers" }) } @@ -325,9 +325,9 @@ export class GeoOperations { [lon0, lat], [lon0, lat0], [lon, lat0], - [lon, lat], - ], - }, + [lon, lat] + ] + } } } @@ -368,9 +368,9 @@ export class GeoOperations { type: "Feature", geometry: { type: "LineString", - coordinates: way.geometry.coordinates[0], + coordinates: way.geometry.coordinates[0] }, - properties: way.properties, + properties: way.properties } } if (way.geometry.type === "MultiPolygon") { @@ -378,9 +378,9 @@ export class GeoOperations { type: "Feature", geometry: { type: "MultiLineString", - coordinates: way.geometry.coordinates[0], + coordinates: way.geometry.coordinates[0] }, - properties: way.properties, + properties: way.properties } } if (way.geometry.type === "LineString") { @@ -512,6 +512,8 @@ export class GeoOperations { /** * Given a list of features, will construct a map of slippy map tile-indices. * Features of which the BBOX overlaps with the corresponding slippy map tile are added to the corresponding array + * + * Also @see clipAllInBox * @param features * @param zoomlevel */ @@ -535,6 +537,33 @@ export class GeoOperations { return perBbox } + /** + * Given a list of features, returns a new list of features so that the features are clipped into the given tile-index. + * Note: IDs are rewritten + * Also @see spreadIntoBBoxes + */ + public static clipAllInBox(features: ReadonlyArray>, tileIndex: number): Feature[] { + const bbox = Tiles.asGeojson(tileIndex) + const newFeatures: Feature[] = [] + for (const f of features) { + const intersectionParts = GeoOperations.clipWith(f, bbox) + for (let i = 0; i < intersectionParts.length; i++) { + const intersectionPart = intersectionParts[i] + let id = (f.properties?.id ?? "") + "_" + tileIndex + if (i > 0) { + id += "_part_" + i + } + const properties = { + ...f.properties, + id + } + intersectionPart.properties = properties + newFeatures.push(intersectionPart) + } + } + return Utils.NoNull(newFeatures) + } + public static toGpx( locations: | Feature @@ -558,8 +587,8 @@ export class GeoOperations { properties: {}, geometry: { type: "Point", - coordinates: p, - }, + coordinates: p + } } ) } @@ -575,7 +604,7 @@ export class GeoOperations { trackPoints.push(trkpt) } const header = - '' + "" return ( header + "\n" + @@ -614,7 +643,7 @@ export class GeoOperations { trackPoints.push(trkpt) } const header = - '' + "" return ( header + "\n" + @@ -640,7 +669,7 @@ export class GeoOperations { const copy = { ...feature, - geometry: { ...feature.geometry }, + geometry: { ...feature.geometry } } let coordinates: [number, number][] if (feature.geometry.type === "LineString") { @@ -698,8 +727,8 @@ export class GeoOperations { type: "Feature", geometry: { type: "LineString", - coordinates: [a, b], - }, + coordinates: [a, b] + } }, distanceMeter, { units: "meters" } @@ -736,17 +765,26 @@ export class GeoOperations { * GeoOperations.completelyWithin(park, pond) // => false */ static completelyWithin( - feature: Feature, - possiblyEnclosingFeature: Feature + feature: Readonly, + possiblyEnclosingFeature: Readonly> ): boolean { + if (feature.geometry.type === "MultiPolygon") { + const polygons = feature.geometry.coordinates.map(coordinates => + >{ + type: "Feature", geometry: { + type: "Polygon", coordinates + } + }) + return !polygons.some(polygon => !booleanWithin(polygon, possiblyEnclosingFeature)) + } return booleanWithin(feature, possiblyEnclosingFeature) } /** * Create an intersection between two features. - * One or multiple new feature is returned based on 'toSplit', which'll have a geometry that is completely withing boundary + * One or multiple new feature are returned based on 'toSplit', which'll have a geometry that is completely withing boundary */ - public static clipWith(toSplit: Feature, boundary: Feature): Feature[] { + public static clipWith(toSplit: Readonly, boundary: Readonly>): Feature[] { if (toSplit.geometry.type === "Point") { const p = >toSplit if (GeoOperations.inside(<[number, number]>p.geometry.coordinates, boundary)) { @@ -757,9 +795,9 @@ export class GeoOperations { } if (toSplit.geometry.type === "LineString") { - const splitup = turf.lineSplit(>toSplit, boundary) - const kept = [] - for (const f of splitup.features) { + const splitup: Feature[] = turf.lineSplit(>toSplit, boundary).features + const kept: Feature[] = [] + for (const f of splitup) { if (!GeoOperations.inside(GeoOperations.centerpointCoordinates(f), boundary)) { continue } @@ -787,7 +825,24 @@ export class GeoOperations { return kept } if (toSplit.geometry.type === "Polygon" || toSplit.geometry.type == "MultiPolygon") { + const splitup = turf.intersect(>toSplit, boundary) + if (splitup === null) { + // No intersection found. + // Either: the boundary is contained fully in 'toSplit', 'toSplit' is contained fully in 'boundary' or they are unrelated at all + if (GeoOperations.completelyWithin(toSplit, boundary)) { + return [toSplit] + } + if (GeoOperations.completelyWithin(boundary, >toSplit)) { + return [{ + type: "Feature", + properties: { ...toSplit.properties }, + geometry: boundary.geometry, + bbox: boundary.bbox + }] + } + return [] + } splitup.properties = { ...toSplit.properties } return [splitup] } @@ -864,32 +919,6 @@ export class GeoOperations { } } - /** - * Constructs all tiles where features overlap with and puts those features in them. - * Long features (e.g. lines or polygons) which overlap with multiple tiles are referenced in each tile they overlap with - * @param zoomlevel - * @param features - */ - public static slice(zoomlevel: number, features: Feature[]): Map { - const tiles = new Map() - - for (const feature of features) { - const bbox = BBox.get(feature) - Tiles.MapRange(Tiles.tileRangeFrom(bbox, zoomlevel), (x, y) => { - const i = Tiles.tile_index(zoomlevel, x, y) - - let tiledata = tiles.get(i) - if (tiledata === undefined) { - tiledata = [] - tiles.set(i, tiledata) - } - tiledata.push(feature) - }) - } - - return tiles - } - /** * Creates a linestring object based on the outer ring of the given polygon * @@ -905,8 +934,8 @@ export class GeoOperations { properties: p.properties, geometry: { type: "LineString", - coordinates: p.geometry.coordinates[0], - }, + coordinates: p.geometry.coordinates[0] + } } } @@ -934,7 +963,7 @@ export class GeoOperations { console.debug("SPlitting way", feature.properties.id) result.push({ ...feature, - geometry: { ...feature.geometry, coordinates: coors.slice(i + 1) }, + geometry: { ...feature.geometry, coordinates: coors.slice(i + 1) } }) coors = coors.slice(0, i + 1) break @@ -943,7 +972,7 @@ export class GeoOperations { } result.push({ ...feature, - geometry: { ...feature.geometry, coordinates: coors }, + geometry: { ...feature.geometry, coordinates: coors } }) } } @@ -1117,8 +1146,8 @@ export class GeoOperations { properties: multiLineStringFeature.properties, geometry: { type: "LineString", - coordinates: coors[0], - }, + coordinates: coors[0] + } } } return { @@ -1126,8 +1155,8 @@ export class GeoOperations { properties: multiLineStringFeature.properties, geometry: { type: "MultiLineString", - coordinates: coors, - }, + coordinates: coors + } } } diff --git a/src/Logic/Web/CommunityIndex.ts b/src/Logic/Web/CommunityIndex.ts new file mode 100644 index 000000000..bb0dc1476 --- /dev/null +++ b/src/Logic/Web/CommunityIndex.ts @@ -0,0 +1,33 @@ +/** + * Various tools and types to work with the community index (https://openstreetmap.community/; https://github.com/osmlab/osm-community-index) + */ + + +export interface CommunityResource { + /** + * A unique identifier for the resource + * "pattern": "^[-_.A-Za-z0-9]+$" + */ + id: string, + /** + * Type of community resource (thus: platform) + */ + type: string, + /** + * included and excluded locations for this item + * See location-conflation documentation for compatible values: https://github.com/rapideditor/location-conflation#readme + */ + locationSet?, + + /** Array of ISO-639-1 (2 letter) or ISO-639-3 (3 letter) codes in lowercase + * */ + languageCodes?: string[] + /** + * Resource account string, required for some resource types + */ + account?: string + + resolved?: { url: string, name: string, description: string } & Record + +} + diff --git a/src/Models/TileRange.ts b/src/Models/TileRange.ts index 6b70c438e..31890210d 100644 --- a/src/Models/TileRange.ts +++ b/src/Models/TileRange.ts @@ -1,4 +1,5 @@ import { BBox } from "../Logic/BBox" +import { Feature, Polygon } from "geojson" export interface TileRange { xstart: number @@ -80,6 +81,17 @@ export class Tiles { return [z, x, index % factor] } + static asGeojson(index: number): Feature; + static asGeojson(x: number, y: number, z: number): Feature; + static asGeojson(zIndex: number, x?: number, y?: number): Feature { + let z = zIndex + if (x === undefined) { + [z, x, y] = Tiles.tile_from_index(zIndex) + } + const bounds = Tiles.tile_bounds_lon_lat(z, x, y) + return new BBox(bounds).asGeoJson() + } + /** * Return x, y of the tile containing (lat, lon) on the given zoom level */ diff --git a/src/UI/BigComponents/CommunityIndexView.svelte b/src/UI/BigComponents/CommunityIndexView.svelte index 5ef59b4f3..d1dfc3659 100644 --- a/src/UI/BigComponents/CommunityIndexView.svelte +++ b/src/UI/BigComponents/CommunityIndexView.svelte @@ -6,8 +6,10 @@ import ContactLink from "./ContactLink.svelte" import { GeoOperations } from "../../Logic/GeoOperations" import Translations from "../i18n/Translations" - import ToSvelte from "../Base/ToSvelte.svelte" import type { Feature, Geometry, GeometryCollection } from "@turf/turf" + import type { FeatureCollection, Polygon } from "geojson" + import type { CommunityResource } from "../../Logic/Web/CommunityIndex" + import Tr from "../Base/Tr.svelte" export let location: Store<{ lat: number; lon: number }> const tileToFetch: Store = location.mapD((l) => { @@ -20,7 +22,10 @@ >([]) tileToFetch.addCallbackAndRun(async (url) => { - const data = await Utils.downloadJsonCached(url, 24 * 60 * 60) + const data = await Utils.downloadJsonCached + }>>(url, 24 * 60 * 60) if (data === undefined) { return } @@ -29,15 +34,13 @@ const filteredResources = resources.map( (features) => - features.filter((f) => { - return GeoOperations.inside([location.data.lon, location.data.lat], f) - }), + features.filter((f) => GeoOperations.inside([location.data.lon, location.data.lat], f)), [location] )
- + {#each $filteredResources as feature} {/each} diff --git a/src/UI/BigComponents/ContactLink.svelte b/src/UI/BigComponents/ContactLink.svelte index ca37679f8..123d5f82a 100644 --- a/src/UI/BigComponents/ContactLink.svelte +++ b/src/UI/BigComponents/ContactLink.svelte @@ -3,23 +3,18 @@ // The _properties_ of a community feature import Locale from "../i18n/Locale.js" import Translations from "../i18n/Translations" - import ToSvelte from "../Base/ToSvelte.svelte" import * as native from "../../assets/language_native.json" import { TypedTranslation } from "../i18n/Translation" import Tr from "../Base/Tr.svelte" + import type { CommunityResource } from "../../Logic/Web/CommunityIndex" const availableTranslationTyped: TypedTranslation<{ native: string }> = Translations.t.communityIndex.available const availableTranslation = availableTranslationTyped.OnEveryLanguage((s, ln) => s.replace("{native}", native[ln] ?? ln) ) - export let country: { resources; nameEn: string } - let resources: { - id: string - resolved: Record - languageCodes: string[] - type: string - }[] = [] + export let country: { resources: Record; nameEn: string } + let resources: CommunityResource[] = [] $: resources = Array.from(Object.values(country?.resources ?? {})) const language = Locale.language diff --git a/test/Logic/GeoOperations.spec.ts b/test/Logic/GeoOperations.spec.ts index f06960365..0bc249be1 100644 --- a/test/Logic/GeoOperations.spec.ts +++ b/test/Logic/GeoOperations.spec.ts @@ -6,7 +6,7 @@ import { describe, expect, it } from "vitest" describe("GeoOperations", () => { describe("calculateOverlap", () => { it("should not give too much overlap (regression test)", () => { - const polyGrb = { + const polyGrb: Feature = { type: "Feature", properties: { osm_id: "25189153", @@ -37,7 +37,7 @@ describe("GeoOperations", () => { "_now:date": "2021-12-05", "_now:datetime": "2021-12-05 21:51:40", "_loaded:date": "2021-12-05", - "_loaded:datetime": "2021-12-05 21:51:40", + "_loaded:datetime": "2021-12-05 21:51:40" }, geometry: { type: "Polygon", @@ -50,21 +50,21 @@ describe("GeoOperations", () => { [3.24329779999996, 50.837435399999855], [3.2431881000000504, 50.83740090000025], [3.243152699999997, 50.83738980000017], - [3.2431059999999974, 50.83730270000021], - ], - ], + [3.2431059999999974, 50.83730270000021] + ] + ] }, id: "https://betadata.grbosm.site/grb?bbox=360935.6475626023,6592540.815539878,361088.52161917265,6592693.689596449/37", _lon: 3.2432137000000116, _lat: 50.83736194999996, bbox: { + minLat: 50.83728850000007, maxLat: 50.837435399999855, maxLon: 3.2433214000000254, - minLat: 50.83728850000007, - minLon: 3.2431059999999974, - }, + minLon: 3.2431059999999974 + } } - const polyHouse = { + const polyHouse: Feature = { type: "Feature", id: "way/594963177", properties: { @@ -95,7 +95,7 @@ describe("GeoOperations", () => { "_loaded:date": "2021-12-05", "_loaded:datetime": "2021-12-05 21:51:39", _surface: "93.32785810484549", - "_surface:ha": "0", + "_surface:ha": "0" }, geometry: { type: "Polygon", @@ -108,9 +108,9 @@ describe("GeoOperations", () => { [3.2431691, 50.8374252], [3.2430936, 50.837401], [3.243046, 50.8374112], - [3.2429993, 50.8373243], - ], - ], + [3.2429993, 50.8373243] + ] + ] }, _lon: 3.2430937, _lat: 50.83736395, @@ -118,8 +118,8 @@ describe("GeoOperations", () => { maxLat: 50.8374252, maxLon: 3.2431881, minLat: 50.8373027, - minLon: 3.2429993, - }, + minLon: 3.2429993 + } } const p0 = turf.polygon(polyGrb.geometry.coordinates) @@ -145,11 +145,11 @@ describe("GeoOperations", () => { [3.218560377159008, 51.21499687768525], [3.2207456783268356, 51.21499687768525], [3.2207456783268356, 51.21600586532159], - [3.218560377159008, 51.21600586532159], - ], + [3.218560377159008, 51.21600586532159] + ] ], - type: "Polygon", - }, + type: "Polygon" + } } const line: Feature = { type: "Feature", @@ -157,10 +157,10 @@ describe("GeoOperations", () => { geometry: { coordinates: [ [3.218405371672816, 51.21499091846559], - [3.2208408127450525, 51.21560173433727], + [3.2208408127450525, 51.21560173433727] ], - type: "LineString", - }, + type: "LineString" + } } const result = GeoOperations.clipWith(line, bbox) expect(result.length).to.equal(1) @@ -168,10 +168,83 @@ describe("GeoOperations", () => { const clippedLine = (>result[0]).geometry.coordinates const expCoordinates = [ [3.2185604, 51.215029800031594], - [3.2207457, 51.21557787977764], + [3.2207457, 51.21557787977764] ] expect(clippedLine).to.deep.equal(expCoordinates) }) + it("clipWith should contain the full feature if it is fully contained", () => { + const bbox: Feature = { + type: "Feature", + properties: {}, + geometry: { + coordinates: [ + [ + [ + 2.1541744759711037, + 51.73994420687188 + ], + [ + 2.1541744759711037, + 50.31129074222787 + ], + [ + 4.53247037641421, + 50.31129074222787 + ], + [ + 4.53247037641421, + 51.73994420687188 + ], + [ + 2.1541744759711037, + 51.73994420687188 + ] + ] + ], + type: "Polygon" + } + } + const content: Feature = { + "type": "Feature", + "properties": {}, + "geometry": { + "coordinates": [ + [ + [ + 2.8900597545854225, + 50.9035099487991 + ], + [ + 3.4872999807053873, + 50.74856284865993 + ], + [ + 3.9512276563531543, + 50.947206170675486 + ], + [ + 3.897902636163167, + 51.25526892606362 + ], + [ + 3.188679867646016, + 51.24525576870511 + ], [ + 2.8900597545854225, + 50.9035099487991 + ] + ] + ], + "type": "Polygon" + } + } + const clipped = GeoOperations.clipWith(content, bbox) + expect(clipped.length).to.equal(1) + + const clippedReverse = GeoOperations.clipWith(bbox, content) + expect(clippedReverse.length).to.equal(1) + } + ) }) })