forked from MapComplete/MapComplete
		
	Add polygon merging
This commit is contained in:
		
							parent
							
								
									ee38cdb9d7
								
							
						
					
					
						commit
						ee3e000cd1
					
				
					 11 changed files with 460 additions and 305 deletions
				
			
		|  | @ -2,43 +2,49 @@ import { Store, UIEventSource } from "../../UIEventSource" | |||
| import { FeatureSource, IndexedFeatureSource } from "../FeatureSource" | ||||
| import { Feature } from "geojson" | ||||
| import { Utils } from "../../../Utils" | ||||
| import DynamicTileSource from "../TiledFeatureSource/DynamicTileSource" | ||||
| 
 | ||||
| /** | ||||
|  * | ||||
|  * The featureSourceMerger receives complete geometries from various sources. | ||||
|  * If multiple sources contain the same object (as determined by 'id'), only one copy of them is retained | ||||
|  */ | ||||
| export default class FeatureSourceMerger implements IndexedFeatureSource { | ||||
| export default class FeatureSourceMerger<Src extends FeatureSource = FeatureSource> implements IndexedFeatureSource { | ||||
|     public features: UIEventSource<Feature[]> = new UIEventSource([]) | ||||
|     public readonly featuresById: Store<Map<string, Feature>> | ||||
|     private readonly _featuresById: UIEventSource<Map<string, Feature>> | ||||
|     private readonly _sources: FeatureSource[] = [] | ||||
|     protected readonly _featuresById: UIEventSource<Map<string, Feature>> | ||||
|     private readonly _sources: Src[] = [] | ||||
|     /** | ||||
|      * Merges features from different featureSources. | ||||
|      * In case that multiple features have the same id, the latest `_version_number` will be used. Otherwise, we will take the last one | ||||
|      */ | ||||
|     constructor(...sources: FeatureSource[]) { | ||||
|     constructor(...sources: Src[]) { | ||||
|         this._featuresById = new UIEventSource<Map<string, Feature>>(new Map<string, Feature>()) | ||||
|         this.featuresById = this._featuresById | ||||
|         const self = this | ||||
|         sources = Utils.NoNull(sources) | ||||
|         for (let source of sources) { | ||||
|             source.features.addCallback(() => { | ||||
|                 self.addData(sources.map((s) => s.features.data)) | ||||
|                 self.addDataFromSources(sources) | ||||
|             }) | ||||
|         } | ||||
|         this.addData(sources.map((s) => s.features.data)) | ||||
|         this.addDataFromSources(sources) | ||||
|         this._sources = sources | ||||
|     } | ||||
| 
 | ||||
|     public addSource(source: FeatureSource) { | ||||
|     public addSource(source: Src) { | ||||
|         if (!source) { | ||||
|             return | ||||
|         } | ||||
|         this._sources.push(source) | ||||
|         source.features.addCallbackAndRun(() => { | ||||
|             this.addData(this._sources.map((s) => s.features.data)) | ||||
|             this.addDataFromSources(this._sources) | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     protected addDataFromSources(sources: Src[]){ | ||||
|         this.addData(sources.map(s => s.features.data)) | ||||
|     } | ||||
| 
 | ||||
|     protected addData(sources: Feature[][]) { | ||||
|         sources = Utils.NoNull(sources) | ||||
|         let somethingChanged = false | ||||
|  | @ -56,7 +62,7 @@ export default class FeatureSourceMerger implements IndexedFeatureSource { | |||
|                 const id = f.properties.id | ||||
|                 unseen.delete(id) | ||||
|                 if (!all.has(id)) { | ||||
|                     // This is a new feature
 | ||||
|                     // This is a new, previously unseen feature
 | ||||
|                     somethingChanged = true | ||||
|                     all.set(id, f) | ||||
|                     continue | ||||
|  | @ -81,10 +87,8 @@ export default class FeatureSourceMerger implements IndexedFeatureSource { | |||
|             return | ||||
|         } | ||||
| 
 | ||||
|         const newList = [] | ||||
|         all.forEach((value) => { | ||||
|             newList.push(value) | ||||
|         }) | ||||
|         const newList = Array.from(all.values()) | ||||
| 
 | ||||
|         this.features.setData(newList) | ||||
|         this._featuresById.setData(all) | ||||
|     } | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import { Feature, Geometry } from "geojson" | ||||
| import { Store, UIEventSource } from "../../UIEventSource" | ||||
| import { FeatureSource } from "../FeatureSource" | ||||
| import { FeatureSourceForTile } from "../FeatureSource" | ||||
| import Pbf from "pbf" | ||||
| import * as pbfCompile from "pbf/compile" | ||||
| import * as PbfSchema from "protocol-buffers-schema" | ||||
|  | @ -19,8 +19,67 @@ class MvtFeatureBuilder { | |||
|         this._y0 = extent * y | ||||
|     } | ||||
| 
 | ||||
|     public toGeoJson(geometry, typeIndex, properties): Feature { | ||||
|         let coords: [number, number] | Coords | Coords[] = this.encodeGeometry(geometry) | ||||
|     private static signedArea(ring: Coords): number { | ||||
|         let sum = 0 | ||||
|         const len = ring.length | ||||
|         // J is basically (i - 1) % len
 | ||||
|         let j = len - 1 | ||||
|         let p1 | ||||
|         let p2 | ||||
|         for (let i = 0; i < len; i++) { | ||||
|             p1 = ring[i] | ||||
|             p2 = ring[j] | ||||
|             sum += (p2.x - p1.x) * (p1.y + p2.y) | ||||
|             j = i | ||||
|         } | ||||
|         return sum | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * | ||||
|      * const rings = [   [     [       3.208361864089966,       51.186908820014736     ],     [       3.2084155082702637,       51.18689537073311     ],     [       3.208436965942383,       51.186888646090836     ],     [       3.2084155082702637,       51.18686174751187     ],     [       3.2084155082702637,       51.18685502286465     ],     [       3.2083725929260254,       51.18686847215807     ],     [       3.2083404064178467,       51.18687519680333     ],     [       3.208361864089966,       51.186908820014736     ]   ] ] | ||||
|      * MvtFeatureBuilder.classifyRings(rings) // => [rings]
 | ||||
|      */ | ||||
|     private static classifyRings(rings: Coords[]): Coords[][] { | ||||
|         if (rings.length <= 0) { | ||||
|             throw "Now rings in polygon found" | ||||
|         } | ||||
|         if (rings.length == 1) { | ||||
|             return [rings] | ||||
|         } | ||||
| 
 | ||||
|         const polygons: Coords[][] = [] | ||||
|         let currentPolygon: Coords[] | ||||
| 
 | ||||
|         for (let i = 0; i < rings.length; i++) { | ||||
|             let ring = rings[i] | ||||
|             const area = this.signedArea(ring) | ||||
|             if (area === 0) { | ||||
|                 // Weird, degenerate ring
 | ||||
|                 continue | ||||
|             } | ||||
|             const ccw = area < 0 | ||||
| 
 | ||||
|             if (ccw === (area < 0)) { | ||||
|                 if (currentPolygon) { | ||||
|                     polygons.push(currentPolygon) | ||||
|                 } | ||||
|                 currentPolygon = [ring] | ||||
| 
 | ||||
|             } else { | ||||
|                 currentPolygon.push(ring) | ||||
|             } | ||||
|         } | ||||
|         if (currentPolygon) { | ||||
|             polygons.push(currentPolygon) | ||||
|         } | ||||
| 
 | ||||
|         return polygons | ||||
|     } | ||||
| 
 | ||||
|     public toGeoJson(geometry: number[], typeIndex: 1 | 2 | 3, properties: any): Feature { | ||||
|         let coords: Coords[] = this.encodeGeometry(geometry) | ||||
|         let classified = undefined | ||||
|         switch (typeIndex) { | ||||
|             case 1: | ||||
|                 const points = [] | ||||
|  | @ -38,9 +97,9 @@ class MvtFeatureBuilder { | |||
|                 break | ||||
| 
 | ||||
|             case 3: | ||||
|                 let classified = this.classifyRings(coords) | ||||
|                 for (let i = 0; i < coords.length; i++) { | ||||
|                     for (let j = 0; j < coords[i].length; j++) { | ||||
|                 classified = MvtFeatureBuilder.classifyRings(coords) | ||||
|                 for (let i = 0; i < classified.length; i++) { | ||||
|                     for (let j = 0; j < classified[i].length; j++) { | ||||
|                         this.project(classified[i][j]) | ||||
|                     } | ||||
|                 } | ||||
|  | @ -48,9 +107,11 @@ class MvtFeatureBuilder { | |||
|         } | ||||
| 
 | ||||
|         let type: string = MvtFeatureBuilder.geom_types[typeIndex] | ||||
|         let polygonCoords: Coords | Coords[] | Coords[][] | ||||
|         if (coords.length === 1) { | ||||
|             coords = coords[0] | ||||
|             polygonCoords = (classified ?? coords)[0] | ||||
|         } else { | ||||
|             polygonCoords = classified ?? coords | ||||
|             type = "Multi" + type | ||||
|         } | ||||
| 
 | ||||
|  | @ -58,13 +119,22 @@ class MvtFeatureBuilder { | |||
|             type: "Feature", | ||||
|             geometry: { | ||||
|                 type: <any>type, | ||||
|                 coordinates: <any>coords, | ||||
|                 coordinates: <any>polygonCoords, | ||||
|             }, | ||||
|             properties, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private encodeGeometry(geometry: number[]) { | ||||
|     /** | ||||
|      * | ||||
|      * const geometry = [9,233,8704,130,438,1455,270,653,248,423,368,493,362,381,330,267,408,301,406,221,402,157,1078,429,1002,449,1036,577,800,545,1586,1165,164,79,40] | ||||
|      * const builder = new MvtFeatureBuilder(4096, 66705, 43755, 17) | ||||
|      * const expected = [[3.2106759399175644,51.213658395282124],[3.2108227908611298,51.21396418776169],[3.2109133154153824,51.21410154168976],[3.210996463894844,51.214190590500664],[3.211119845509529,51.214294340548975],[3.211241215467453,51.2143745681588],[3.2113518565893173,51.21443085341426],[3.211488649249077,51.21449427925393],[3.2116247713565826,51.214540903490956],[3.211759552359581,51.21457408647774],[3.2121209800243378,51.214664394485254],[3.212456926703453,51.21475890267553],[3.2128042727708817,51.214880292910834],[3.213072493672371,51.214994962285544],[3.2136042416095734,51.21523984134939],[3.2136592268943787,51.21525664260963],[3.213672637939453,51.21525664260963]] | ||||
|      * builder.project(builder.encodeGeometry(geometry)[0]) // => expected
 | ||||
|      * @param geometry | ||||
|      * @private | ||||
|      */ | ||||
|     private encodeGeometry(geometry: number[]): Coords[] { | ||||
|         let cX = 0 | ||||
|         let cY = 0 | ||||
|         let coordss: Coords[] = [] | ||||
|  | @ -86,7 +156,7 @@ class MvtFeatureBuilder { | |||
|                     currentRing = [] | ||||
|                 } | ||||
|             } | ||||
|             if (commandId === 1 || commandId === 2){ | ||||
|             if (commandId === 1 || commandId === 2) { | ||||
|                 for (let j = 0; j < commandCount; j++) { | ||||
|                     const dx = geometry[i + j * 2 + 1] | ||||
|                     cX += ((dx >> 1) ^ (-(dx & 1))) | ||||
|  | @ -94,10 +164,11 @@ class MvtFeatureBuilder { | |||
|                     cY += ((dy >> 1) ^ (-(dy & 1))) | ||||
|                     currentRing.push([cX, cY]) | ||||
|                 } | ||||
|                 i = commandCount * 2 | ||||
|                 i += commandCount * 2 | ||||
|             } | ||||
|             if(commandId === 7){ | ||||
|             if (commandId === 7) { | ||||
|                 currentRing.push([...currentRing[0]]) | ||||
|                 i++ | ||||
|             } | ||||
| 
 | ||||
|         } | ||||
|  | @ -107,62 +178,12 @@ class MvtFeatureBuilder { | |||
|         return coordss | ||||
|     } | ||||
| 
 | ||||
|     private signedArea(ring: Coords): number { | ||||
|         let sum = 0 | ||||
|         const len = ring.length | ||||
|         // J is basically (i - 1) % len
 | ||||
|         let j = len - 1 | ||||
|         let p1 | ||||
|         let p2 | ||||
|         for (let i = 0; i < len; i++) { | ||||
|             p1 = ring[i] | ||||
|             p2 = ring[j] | ||||
|             sum += (p2.x - p1.x) * (p1.y + p2.y) | ||||
|             j = i | ||||
|         } | ||||
|         return sum | ||||
|     } | ||||
| 
 | ||||
|     private classifyRings(rings: Coords[]): Coords[][] { | ||||
|         const len = rings.length | ||||
| 
 | ||||
|         if (len <= 1) return [rings] | ||||
| 
 | ||||
|         const polygons = [] | ||||
|         let polygon | ||||
|         // CounterClockWise
 | ||||
|         let ccw: boolean | ||||
| 
 | ||||
|         for (let i = 0; i < len; i++) { | ||||
|             const area = this.signedArea(rings[i]) | ||||
|             if (area === 0) continue | ||||
| 
 | ||||
|             if (ccw === undefined) { | ||||
|                 ccw = area < 0 | ||||
|             } | ||||
|             if (ccw === (area < 0)) { | ||||
|                 if (polygon) { | ||||
|                     polygons.push(polygon) | ||||
|                 } | ||||
|                 polygon = [rings[i]] | ||||
| 
 | ||||
|             } else { | ||||
|                 polygon.push(rings[i]) | ||||
|             } | ||||
|         } | ||||
|         if (polygon) { | ||||
|             polygons.push(polygon) | ||||
|         } | ||||
| 
 | ||||
|         return polygons | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Inline replacement of the location by projecting | ||||
|      * @param line | ||||
|      * @private | ||||
|      * @param line the line which will be rewritten inline | ||||
|      * @return line | ||||
|      */ | ||||
|     private project(line: [number, number][]) { | ||||
|     private project(line: Coords) { | ||||
|         const y0 = this._y0 | ||||
|         const x0 = this._x0 | ||||
|         const size = this._size | ||||
|  | @ -174,12 +195,13 @@ class MvtFeatureBuilder { | |||
|                 360 / Math.PI * Math.atan(Math.exp(y2 * Math.PI / 180)) - 90, | ||||
|             ] | ||||
|         } | ||||
|         return line | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export default class MvtSource implements FeatureSource { | ||||
| export default class MvtSource implements FeatureSourceForTile { | ||||
| 
 | ||||
|     private static readonly schemaSpec = ` | ||||
|     private static readonly schemaSpec21 = ` | ||||
|     package vector_tile; | ||||
| 
 | ||||
| option optimize_for = LITE_RUNTIME; | ||||
|  | @ -259,26 +281,30 @@ message Tile { | |||
|         extensions 16 to 8191; | ||||
| } | ||||
| ` | ||||
|     private static readonly tile_schema = pbfCompile(PbfSchema.parse(MvtSource.schemaSpec)).Tile | ||||
| 
 | ||||
| 
 | ||||
|     private static readonly tile_schema = (pbfCompile.default ?? pbfCompile)(PbfSchema.parse(MvtSource.schemaSpec21)).Tile | ||||
|     public readonly features: Store<Feature<Geometry, { [name: string]: any }>[]> | ||||
|     private readonly _url: string | ||||
|     private readonly _layerName: string | ||||
|     private readonly _features: UIEventSource<Feature<Geometry, { | ||||
|         [name: string]: any | ||||
|     }>[]> = new UIEventSource<Feature<Geometry, { [p: string]: any }>[]>([]) | ||||
|     public readonly features: Store<Feature<Geometry, { [name: string]: any }>[]> = this._features | ||||
|     private readonly x: number | ||||
|     private readonly y: number | ||||
|     private readonly z: number | ||||
|     public readonly x: number | ||||
|     public readonly y: number | ||||
|     public readonly z: number | ||||
| 
 | ||||
|     constructor(url: string, x: number, y: number, z: number, layerName?: string) { | ||||
|     constructor(url: string, x: number, y: number, z: number, layerName?: string, isActive?: Store<boolean>) { | ||||
|         this._url = url | ||||
|         this._layerName = layerName | ||||
|         this.x = x | ||||
|         this.y = y | ||||
|         this.z = z | ||||
|         this.downloadSync() | ||||
|         this.features = this._features.map(fs => { | ||||
|             if (fs === undefined || isActive?.data === false) { | ||||
|                 return [] | ||||
|             } | ||||
|             return fs | ||||
|         }, [isActive]) | ||||
|     } | ||||
| 
 | ||||
|     private getValue(v: { | ||||
|  | @ -316,16 +342,23 @@ message Tile { | |||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     private downloadSync(){ | ||||
|     private downloadSync() { | ||||
|         this.download().then(d => { | ||||
|             if(d.length === 0){ | ||||
|             if (d.length === 0) { | ||||
|                 return | ||||
|             } | ||||
|             return this._features.setData(d) | ||||
|         }).catch(e => {console.error(e)}) | ||||
|         }).catch(e => { | ||||
|             console.error(e) | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     private async download(): Promise<Feature[]> { | ||||
|         const result = await fetch(this._url) | ||||
|         if (result.status !== 200) { | ||||
|             console.error("Could not download tile " + this._url) | ||||
|             return [] | ||||
|         } | ||||
|         const buffer = await result.arrayBuffer() | ||||
|         const data = MvtSource.tile_schema.read(new Pbf(buffer)) | ||||
|         const layers = data.layers | ||||
|  | @ -336,7 +369,7 @@ message Tile { | |||
|             } | ||||
|             layer = layers.find(l => l.name === this._layerName) | ||||
|         } | ||||
|         if(!layer){ | ||||
|         if (!layer) { | ||||
|             return [] | ||||
|         } | ||||
|         const builder = new MvtFeatureBuilder(layer.extent, this.x, this.y, this.z) | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue