forked from MapComplete/MapComplete
		
	Add clipping to generateCache
This commit is contained in:
		
							parent
							
								
									f7f0ccdb7d
								
							
						
					
					
						commit
						509b237d02
					
				
					 3 changed files with 177 additions and 90 deletions
				
			
		|  | @ -15,6 +15,11 @@ import togpx from "togpx" | |||
| import Constants from "../Models/Constants" | ||||
| 
 | ||||
| export class GeoOperations { | ||||
|     /** | ||||
|      * Create a union between two features | ||||
|      */ | ||||
|     static union = turf.union | ||||
|     static intersect = turf.intersect | ||||
|     private static readonly _earthRadius = 6378137 | ||||
|     private static readonly _originShift = (2 * Math.PI * GeoOperations._earthRadius) / 2 | ||||
| 
 | ||||
|  | @ -158,35 +163,6 @@ export class GeoOperations { | |||
|         return result | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Helper function which does the heavy lifting for 'inside' | ||||
|      */ | ||||
|     private static pointInPolygonCoordinates( | ||||
|         x: number, | ||||
|         y: number, | ||||
|         coordinates: [number, number][][] | ||||
|     ) { | ||||
|         const inside = GeoOperations.pointWithinRing( | ||||
|             x, | ||||
|             y, | ||||
|             /*This is the outer ring of the polygon */ coordinates[0] | ||||
|         ) | ||||
|         if (!inside) { | ||||
|             return false | ||||
|         } | ||||
|         for (let i = 1; i < coordinates.length; i++) { | ||||
|             const inHole = GeoOperations.pointWithinRing( | ||||
|                 x, | ||||
|                 y, | ||||
|                 coordinates[i] /* These are inner rings, aka holes*/ | ||||
|             ) | ||||
|             if (inHole) { | ||||
|                 return false | ||||
|             } | ||||
|         } | ||||
|         return true | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Detect wether or not the given point is located in the feature | ||||
|      * | ||||
|  | @ -620,6 +596,113 @@ export class GeoOperations { | |||
|         return copy | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Takes two points and finds the geographic bearing between them, i.e. the angle measured in degrees from the north line (0 degrees) | ||||
|      */ | ||||
|     public static bearing(a: Coord, b: Coord): number { | ||||
|         return turf.bearing(a, b) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns 'true' if one feature contains the other feature | ||||
|      * | ||||
|      * const pond: Feature<Polygon, any> = { | ||||
|      *       "type": "Feature", | ||||
|      *       "properties": {"natural":"water","water":"pond"}, | ||||
|      *       "geometry": { | ||||
|      *         "type": "Polygon", | ||||
|      *         "coordinates": [[ | ||||
|      *             [4.362924098968506,50.8435422298544 ], | ||||
|      *             [4.363272786140442,50.8435219059949 ], | ||||
|      *             [4.363213777542114,50.8437420806679 ], | ||||
|      *             [4.362924098968506,50.8435422298544 ] | ||||
|      *           ]]}} | ||||
|      * const park: Feature<Polygon, any> =   { | ||||
|      *       "type": "Feature", | ||||
|      *       "properties": {"leisure":"park"}, | ||||
|      *       "geometry": { | ||||
|      *         "type": "Polygon", | ||||
|      *         "coordinates": [[ | ||||
|      *            [ 4.36073541641235,50.84323737103244 ], | ||||
|      *            [ 4.36469435691833, 50.8423905305197 ], | ||||
|      *            [ 4.36659336090087, 50.8458997374786 ], | ||||
|      *            [ 4.36254858970642, 50.8468007074916 ], | ||||
|      *            [ 4.36073541641235, 50.8432373710324 ] | ||||
|      *           ]]}} | ||||
|      * GeoOperations.completelyWithin(pond, park) // => true
 | ||||
|      * GeoOperations.completelyWithin(park, pond) // => false
 | ||||
|      */ | ||||
|     static completelyWithin( | ||||
|         feature: Feature<Geometry, any>, | ||||
|         possiblyEncloingFeature: Feature<Polygon | MultiPolygon, any> | ||||
|     ): boolean { | ||||
|         return booleanWithin(feature, possiblyEncloingFeature) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Create an intersection between two features. | ||||
|      * A new feature is returned based on 'toSplit', which'll have a geometry that is completely withing boundary | ||||
|      */ | ||||
|     public static clipWith(toSplit: Feature, boundary: Feature<Polygon>): Feature[] { | ||||
|         if (toSplit.geometry.type === "Point") { | ||||
|             const p = <Feature<Point>>toSplit | ||||
|             if (GeoOperations.inside(p.geometry.coordinates, boundary)) { | ||||
|                 return [p] | ||||
|             } else { | ||||
|                 return [] | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (toSplit.geometry.type === "LineString") { | ||||
|             const splitup = turf.lineSplit(<Feature<LineString>>toSplit, boundary) | ||||
|             const kept = [] | ||||
|             for (const f of splitup.features) { | ||||
|                 const ls = <Feature<LineString>>f | ||||
|                 if (!GeoOperations.inside(GeoOperations.centerpointCoordinates(f), boundary)) { | ||||
|                     continue | ||||
|                 } | ||||
|                 f.properties = { ...toSplit.properties } | ||||
|                 kept.push(f) | ||||
|             } | ||||
|             return kept | ||||
|         } | ||||
|         if (toSplit.geometry.type === "Polygon" || toSplit.geometry.type == "MultiPolygon") { | ||||
|             const splitup = turf.intersect(<Feature<Polygon>>toSplit, boundary) | ||||
|             splitup.properties = { ...toSplit.properties } | ||||
|             return [splitup] | ||||
|         } | ||||
|         throw "Invalid geometry type with GeoOperations.clipWith: " + toSplit.geometry.type | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Helper function which does the heavy lifting for 'inside' | ||||
|      */ | ||||
|     private static pointInPolygonCoordinates( | ||||
|         x: number, | ||||
|         y: number, | ||||
|         coordinates: [number, number][][] | ||||
|     ) { | ||||
|         const inside = GeoOperations.pointWithinRing( | ||||
|             x, | ||||
|             y, | ||||
|             /*This is the outer ring of the polygon */ coordinates[0] | ||||
|         ) | ||||
|         if (!inside) { | ||||
|             return false | ||||
|         } | ||||
|         for (let i = 1; i < coordinates.length; i++) { | ||||
|             const inHole = GeoOperations.pointWithinRing( | ||||
|                 x, | ||||
|                 y, | ||||
|                 coordinates[i] /* These are inner rings, aka holes*/ | ||||
|             ) | ||||
|             if (inHole) { | ||||
|                 return false | ||||
|             } | ||||
|         } | ||||
|         return true | ||||
|     } | ||||
| 
 | ||||
|     private static pointWithinRing(x: number, y: number, ring: [number, number][]) { | ||||
|         let inside = false | ||||
|         for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) { | ||||
|  | @ -740,57 +823,4 @@ export class GeoOperations { | |||
|         } | ||||
|         throw "CalculateIntersection fallthrough: can not calculate an intersection between features" | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Takes two points and finds the geographic bearing between them, i.e. the angle measured in degrees from the north line (0 degrees) | ||||
|      */ | ||||
|     public static bearing(a: Coord, b: Coord): number { | ||||
|         return turf.bearing(a, b) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns 'true' if one feature contains the other feature | ||||
|      * | ||||
|      * const pond: Feature<Polygon, any> = { | ||||
|      *       "type": "Feature", | ||||
|      *       "properties": {"natural":"water","water":"pond"}, | ||||
|      *       "geometry": { | ||||
|      *         "type": "Polygon", | ||||
|      *         "coordinates": [[ | ||||
|      *             [4.362924098968506,50.8435422298544 ], | ||||
|      *             [4.363272786140442,50.8435219059949 ], | ||||
|      *             [4.363213777542114,50.8437420806679 ], | ||||
|      *             [4.362924098968506,50.8435422298544 ] | ||||
|      *           ]]}} | ||||
|      * const park: Feature<Polygon, any> =   { | ||||
|      *       "type": "Feature", | ||||
|      *       "properties": {"leisure":"park"}, | ||||
|      *       "geometry": { | ||||
|      *         "type": "Polygon", | ||||
|      *         "coordinates": [[ | ||||
|      *            [ 4.36073541641235,50.84323737103244 ], | ||||
|      *            [ 4.36469435691833, 50.8423905305197 ], | ||||
|      *            [ 4.36659336090087, 50.8458997374786 ], | ||||
|      *            [ 4.36254858970642, 50.8468007074916 ], | ||||
|      *            [ 4.36073541641235, 50.8432373710324 ] | ||||
|      *           ]]}} | ||||
|      * GeoOperations.completelyWithin(pond, park) // => true
 | ||||
|      * GeoOperations.completelyWithin(park, pond) // => false
 | ||||
|      */ | ||||
|     static completelyWithin( | ||||
|         feature: Feature<Geometry, any>, | ||||
|         possiblyEncloingFeature: Feature<Polygon | MultiPolygon, any> | ||||
|     ): boolean { | ||||
|         return booleanWithin(feature, possiblyEncloingFeature) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Create a union between two features | ||||
|      */ | ||||
|     static union = turf.union | ||||
| 
 | ||||
|     /** | ||||
|      * Create an intersection between two features | ||||
|      */ | ||||
|     static intersect = turf.intersect | ||||
| } | ||||
|  |  | |||
|  | @ -24,6 +24,9 @@ import { GeoOperations } from "../Logic/GeoOperations" | |||
| import SimpleMetaTaggers from "../Logic/SimpleMetaTagger" | ||||
| import FilteringFeatureSource from "../Logic/FeatureSource/Sources/FilteringFeatureSource" | ||||
| import Loc from "../Models/Loc" | ||||
| import { Feature } from "geojson" | ||||
| import { BBox } from "../Logic/BBox" | ||||
| import { bboxClip } from "@turf/turf" | ||||
| 
 | ||||
| ScriptUtils.fixUtils() | ||||
| 
 | ||||
|  | @ -232,7 +235,8 @@ function sliceToTiles( | |||
|     theme: LayoutConfig, | ||||
|     relationsTracker: RelationsTracker, | ||||
|     targetdir: string, | ||||
|     pointsOnlyLayers: string[] | ||||
|     pointsOnlyLayers: string[], | ||||
|     clip: boolean | ||||
| ) { | ||||
|     const skippedLayers = new Set<string>() | ||||
| 
 | ||||
|  | @ -310,6 +314,7 @@ function sliceToTiles( | |||
|             maxFeatureCount: undefined, | ||||
|             registerTile: (tile) => { | ||||
|                 const tileIndex = tile.tileIndex | ||||
|                 const bbox = BBox.fromTileIndex(tileIndex).asGeoJson({}) | ||||
|                 console.log("Got tile:", tileIndex, tile.layer.layerDef.id) | ||||
|                 if (tile.features.data.length === 0) { | ||||
|                     return | ||||
|  | @ -343,9 +348,9 @@ function sliceToTiles( | |||
|                 } | ||||
|                 let strictlyCalculated = 0 | ||||
|                 let featureCount = 0 | ||||
|                 for (const feature of filteredTile.features.data) { | ||||
|                 let features: Feature[] = filteredTile.features.data.map((f) => f.feature) | ||||
|                 for (const feature of features) { | ||||
|                     // Some cleanup
 | ||||
|                     delete feature.feature["bbox"] | ||||
| 
 | ||||
|                     if (tile.layer.layerDef.calculatedTags !== undefined) { | ||||
|                         // Evaluate all the calculated tags strictly
 | ||||
|  | @ -353,7 +358,7 @@ function sliceToTiles( | |||
|                             (ct) => ct[0] | ||||
|                         ) | ||||
|                         featureCount++ | ||||
|                         const props = feature.feature.properties | ||||
|                         const props = feature.properties | ||||
|                         for (const calculatedTagKey of calculatedTagKeys) { | ||||
|                             const strict = props[calculatedTagKey] | ||||
| 
 | ||||
|  | @ -379,7 +384,16 @@ function sliceToTiles( | |||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                     delete feature["bbox"] | ||||
|                 } | ||||
| 
 | ||||
|                 if (clip) { | ||||
|                     console.log("Clipping features") | ||||
|                     features = [].concat( | ||||
|                         ...features.map((f: Feature) => GeoOperations.clipWith(<any>f, bbox)) | ||||
|                     ) | ||||
|                 } | ||||
| 
 | ||||
|                 // Lets save this tile!
 | ||||
|                 const [z, x, y] = Tiles.tile_from_index(tileIndex) | ||||
|                 // console.log("Writing tile ", z, x, y, layerId)
 | ||||
|  | @ -391,7 +405,7 @@ function sliceToTiles( | |||
|                     JSON.stringify( | ||||
|                         { | ||||
|                             type: "FeatureCollection", | ||||
|                             features: filteredTile.features.data.map((f) => f.feature), | ||||
|                             features, | ||||
|                         }, | ||||
|                         null, | ||||
|                         " " | ||||
|  | @ -476,8 +490,9 @@ export async function main(args: string[]) { | |||
|     console.log("Cache builder started with args ", args.join(", ")) | ||||
|     if (args.length < 6) { | ||||
|         console.error( | ||||
|             "Expected arguments are: theme zoomlevel targetdirectory lat0 lon0 lat1 lon1 [--generate-point-overview layer-name,layer-name,...] [--force-zoom-level z] \n" + | ||||
|                 "Note: a new directory named <theme> will be created in targetdirectory" | ||||
|             "Expected arguments are: theme zoomlevel targetdirectory lat0 lon0 lat1 lon1 [--generate-point-overview layer-name,layer-name,...] [--force-zoom-level z] [--clip]" + | ||||
|                 "--force-zoom-level causes non-cached-layers to be donwnloaded\n" + | ||||
|                 "--clip will erase parts of the feature falling outside of the bounding box" | ||||
|         ) | ||||
|         return | ||||
|     } | ||||
|  | @ -494,6 +509,7 @@ export async function main(args: string[]) { | |||
|     const lon0 = Number(args[4]) | ||||
|     const lat1 = Number(args[5]) | ||||
|     const lon1 = Number(args[6]) | ||||
|     const clip = args.indexOf("--clip") >= 0 | ||||
| 
 | ||||
|     if (isNaN(lat0)) { | ||||
|         throw "The first number (a latitude) is not a valid number" | ||||
|  | @ -570,7 +586,7 @@ export async function main(args: string[]) { | |||
| 
 | ||||
|     const extraFeatures = await downloadExtraData(theme) | ||||
|     const allFeaturesSource = loadAllTiles(targetdir, tileRange, theme, extraFeatures) | ||||
|     sliceToTiles(allFeaturesSource, theme, relationTracker, targetdir, generatePointLayersFor) | ||||
|     sliceToTiles(allFeaturesSource, theme, relationTracker, targetdir, generatePointLayersFor, clip) | ||||
| } | ||||
| 
 | ||||
| let args = [...process.argv] | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ import { describe } from "mocha" | |||
| import { expect } from "chai" | ||||
| import * as turf from "@turf/turf" | ||||
| import { GeoOperations } from "../../Logic/GeoOperations" | ||||
| import { Feature, LineString, Polygon } from "geojson" | ||||
| 
 | ||||
| describe("GeoOperations", () => { | ||||
|     describe("calculateOverlap", () => { | ||||
|  | @ -133,4 +134,44 @@ describe("GeoOperations", () => { | |||
|             expect(overlapsRev).empty | ||||
|         }) | ||||
|     }) | ||||
|     describe("clipWith", () => { | ||||
|         it("clipWith should clip linestrings", () => { | ||||
|             const bbox: Feature<Polygon> = { | ||||
|                 type: "Feature", | ||||
|                 properties: {}, | ||||
|                 geometry: { | ||||
|                     coordinates: [ | ||||
|                         [ | ||||
|                             [3.218560377159008, 51.21600586532159], | ||||
|                             [3.218560377159008, 51.21499687768525], | ||||
|                             [3.2207456783268356, 51.21499687768525], | ||||
|                             [3.2207456783268356, 51.21600586532159], | ||||
|                             [3.218560377159008, 51.21600586532159], | ||||
|                         ], | ||||
|                     ], | ||||
|                     type: "Polygon", | ||||
|                 }, | ||||
|             } | ||||
|             const line: Feature<LineString> = { | ||||
|                 type: "Feature", | ||||
|                 properties: {}, | ||||
|                 geometry: { | ||||
|                     coordinates: [ | ||||
|                         [3.218405371672816, 51.21499091846559], | ||||
|                         [3.2208408127450525, 51.21560173433727], | ||||
|                     ], | ||||
|                     type: "LineString", | ||||
|                 }, | ||||
|             } | ||||
|             const result = GeoOperations.clipWith(line, bbox) | ||||
|             expect(result.length).to.equal(1) | ||||
|             expect(result[0].geometry.type).to.eq("LineString") | ||||
|             const clippedLine = (<Feature<LineString>>result[0]).geometry.coordinates | ||||
|             const expCoordinates = [ | ||||
|                 [3.2185480732975975, 51.21502965337126], | ||||
|                 [3.2207456783252724, 51.2155808773463], | ||||
|             ] | ||||
|             expect(clippedLine).to.deep.equal(expCoordinates) | ||||
|         }) | ||||
|     }) | ||||
| }) | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue