forked from MapComplete/MapComplete
		
	Merge master
This commit is contained in:
		
						commit
						adaedcba25
					
				
					 27 changed files with 674 additions and 433 deletions
				
			
		|  | @ -249,6 +249,13 @@ export class BBox { | |||
|         ] | ||||
|     } | ||||
| 
 | ||||
|     toLngLatFlat(): [number, number, number, number] { | ||||
|         return [ | ||||
|             this.minLon, this.minLat, | ||||
|             this.maxLon, this.maxLat, | ||||
|         ] | ||||
|     } | ||||
| 
 | ||||
|     public asGeojsonCached() { | ||||
|         if (this["geojsonCache"] === undefined) { | ||||
|             this["geojsonCache"] = this.asGeoJson({}) | ||||
|  |  | |||
|  | @ -2,332 +2,8 @@ import { Feature as GeojsonFeature, Geometry } from "geojson" | |||
| 
 | ||||
| import { Store, UIEventSource } from "../../UIEventSource" | ||||
| import { FeatureSourceForTile, UpdatableFeatureSource } from "../FeatureSource" | ||||
| import Pbf from "pbf" | ||||
| import { MvtToGeojson } from "mvt-to-geojson" | ||||
| 
 | ||||
| type Coords = [number, number][] | ||||
| 
 | ||||
| class MvtFeatureBuilder { | ||||
|     private static readonly geom_types = ["Unknown", "Point", "LineString", "Polygon"] as const | ||||
|     private readonly _size: number | ||||
|     private readonly _x0: number | ||||
|     private readonly _y0: number | ||||
| 
 | ||||
|     constructor(extent: number, x: number, y: number, z: number) { | ||||
|         this._size = extent * Math.pow(2, z) | ||||
|         this._x0 = extent * x | ||||
|         this._y0 = extent * y | ||||
|     } | ||||
| 
 | ||||
|     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): GeojsonFeature { | ||||
|         let coords: Coords[] = this.encodeGeometry(geometry) | ||||
|         let classified = undefined | ||||
|         switch (typeIndex) { | ||||
|             case 1: | ||||
|                 const points = [] | ||||
|                 for (let i = 0; i < coords.length; i++) { | ||||
|                     points[i] = coords[i][0] | ||||
|                 } | ||||
|                 coords = points | ||||
|                 this.project(<any>coords) | ||||
|                 break | ||||
| 
 | ||||
|             case 2: | ||||
|                 for (let i = 0; i < coords.length; i++) { | ||||
|                     this.project(coords[i]) | ||||
|                 } | ||||
|                 break | ||||
| 
 | ||||
|             case 3: | ||||
|                 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]) | ||||
|                     } | ||||
|                 } | ||||
|                 break | ||||
|         } | ||||
| 
 | ||||
|         let type: string = MvtFeatureBuilder.geom_types[typeIndex] | ||||
|         let polygonCoords: Coords | Coords[] | Coords[][] | ||||
|         if (coords.length === 1) { | ||||
|             polygonCoords = (classified ?? coords)[0] | ||||
|         } else { | ||||
|             polygonCoords = classified ?? coords | ||||
|             type = "Multi" + type | ||||
|         } | ||||
| 
 | ||||
|         return { | ||||
|             type: "Feature", | ||||
|             geometry: { | ||||
|                 type: <any>type, | ||||
|                 coordinates: <any>polygonCoords, | ||||
|             }, | ||||
|             properties, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * | ||||
|      * 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 | ||||
|         const coordss: Coords[] = [] | ||||
|         let currentRing: Coords = [] | ||||
|         for (let i = 0; i < geometry.length; i++) { | ||||
|             const commandInteger = geometry[i] | ||||
|             const commandId = commandInteger & 0x7 | ||||
|             const commandCount = commandInteger >> 3 | ||||
|             /* | ||||
|             Command 	Id 	Parameters 	Parameter Count | ||||
|                         MoveTo 	1 	dX, dY 	2 | ||||
|                         LineTo 	2 	dX, dY 	2 | ||||
|                         ClosePath 	7 	No parameters 	0 | ||||
|             */ | ||||
|             if (commandId === 1) { | ||||
|                 // MoveTo means: we start a new ring
 | ||||
|                 if (currentRing.length !== 0) { | ||||
|                     coordss.push(currentRing) | ||||
|                     currentRing = [] | ||||
|                 } | ||||
|             } | ||||
|             if (commandId === 1 || commandId === 2) { | ||||
|                 for (let j = 0; j < commandCount; j++) { | ||||
|                     const dx = geometry[i + j * 2 + 1] | ||||
|                     cX += (dx >> 1) ^ -(dx & 1) | ||||
|                     const dy = geometry[i + j * 2 + 2] | ||||
|                     cY += (dy >> 1) ^ -(dy & 1) | ||||
|                     currentRing.push([cX, cY]) | ||||
|                 } | ||||
|                 i += commandCount * 2 | ||||
|             } | ||||
|             if (commandId === 7) { | ||||
|                 if (currentRing.length === 0) { | ||||
|                     console.error( | ||||
|                         "Invalid MVT file: got a 'closePath', but the currentRing is empty. Full command:", | ||||
|                         commandInteger | ||||
|                     ) | ||||
|                 } else { | ||||
|                     currentRing.push([...currentRing[0]]) | ||||
|                 } | ||||
|                 i++ | ||||
|             } | ||||
|         } | ||||
|         if (currentRing.length > 0) { | ||||
|             coordss.push(currentRing) | ||||
|         } | ||||
|         return coordss | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Inline replacement of the location by projecting | ||||
|      * @param line the line which will be rewritten inline | ||||
|      * @return line | ||||
|      */ | ||||
|     private project(line: Coords) { | ||||
|         const y0 = this._y0 | ||||
|         const x0 = this._x0 | ||||
|         const size = this._size | ||||
|         for (let i = 0; i < line.length; i++) { | ||||
|             let p = line[i] | ||||
|             let y2 = 180 - ((p[1] + y0) * 360) / size | ||||
|             line[i] = [ | ||||
|                 ((p[0] + x0) * 360) / size - 180, | ||||
|                 (360 / Math.PI) * Math.atan(Math.exp((y2 * Math.PI) / 180)) - 90, | ||||
|             ] | ||||
|         } | ||||
|         return line | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| class Layer { | ||||
|     public static read(pbf, end) { | ||||
|         return pbf.readFields( | ||||
|             Layer._readField, | ||||
|             { version: 0, name: "", features: [], keys: [], values: [], extent: 0 }, | ||||
|             end | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     static _readField(tag, obj, pbf) { | ||||
|         if (tag === 15) obj.version = pbf.readVarint() | ||||
|         else if (tag === 1) obj.name = pbf.readString() | ||||
|         else if (tag === 2) obj.features.push(Feature.read(pbf, pbf.readVarint() + pbf.pos)) | ||||
|         else if (tag === 3) obj.keys.push(pbf.readString()) | ||||
|         else if (tag === 4) obj.values.push(Value.read(pbf, pbf.readVarint() + pbf.pos)) | ||||
|         else if (tag === 5) obj.extent = pbf.readVarint() | ||||
|     } | ||||
| 
 | ||||
|     public static write(obj, pbf) { | ||||
|         if (obj.version) pbf.writeVarintField(15, obj.version) | ||||
|         if (obj.name) pbf.writeStringField(1, obj.name) | ||||
|         if (obj.features) | ||||
|             for (var i = 0; i < obj.features.length; i++) | ||||
|                 pbf.writeMessage(2, Feature.write, obj.features[i]) | ||||
|         if (obj.keys) for (i = 0; i < obj.keys.length; i++) pbf.writeStringField(3, obj.keys[i]) | ||||
|         if (obj.values) | ||||
|             for (i = 0; i < obj.values.length; i++) pbf.writeMessage(4, Value.write, obj.values[i]) | ||||
|         if (obj.extent) pbf.writeVarintField(5, obj.extent) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| class Feature { | ||||
|     static read(pbf, end) { | ||||
|         return pbf.readFields(Feature._readField, { id: 0, tags: [], type: 0, geometry: [] }, end) | ||||
|     } | ||||
| 
 | ||||
|     static _readField(tag, obj, pbf) { | ||||
|         if (tag === 1) obj.id = pbf.readVarint() | ||||
|         else if (tag === 2) pbf.readPackedVarint(obj.tags) | ||||
|         else if (tag === 3) obj.type = pbf.readVarint() | ||||
|         else if (tag === 4) pbf.readPackedVarint(obj.geometry) | ||||
|     } | ||||
| 
 | ||||
|     public static write(obj, pbf) { | ||||
|         if (obj.id) pbf.writeVarintField(1, obj.id) | ||||
|         if (obj.tags) pbf.writePackedVarint(2, obj.tags) | ||||
|         if (obj.type) pbf.writeVarintField(3, obj.type) | ||||
|         if (obj.geometry) pbf.writePackedVarint(4, obj.geometry) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| class Value { | ||||
|     public static read(pbf, end) { | ||||
|         return pbf.readFields( | ||||
|             Value._readField, | ||||
|             { | ||||
|                 string_value: "", | ||||
|                 float_value: 0, | ||||
|                 double_value: 0, | ||||
|                 int_value: 0, | ||||
|                 uint_value: 0, | ||||
|                 sint_value: 0, | ||||
|                 bool_value: false, | ||||
|             }, | ||||
|             end | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     static _readField = function (tag, obj, pbf) { | ||||
|         if (tag === 1) obj.string_value = pbf.readString() | ||||
|         else if (tag === 2) obj.float_value = pbf.readFloat() | ||||
|         else if (tag === 3) obj.double_value = pbf.readDouble() | ||||
|         else if (tag === 4) obj.int_value = pbf.readVarint(true) | ||||
|         else if (tag === 5) obj.uint_value = pbf.readVarint() | ||||
|         else if (tag === 6) obj.sint_value = pbf.readSVarint() | ||||
|         else if (tag === 7) obj.bool_value = pbf.readBoolean() | ||||
|     } | ||||
| 
 | ||||
|     public static write(obj, pbf) { | ||||
|         if (obj.string_value) pbf.writeStringField(1, obj.string_value) | ||||
|         if (obj.float_value) pbf.writeFloatField(2, obj.float_value) | ||||
|         if (obj.double_value) pbf.writeDoubleField(3, obj.double_value) | ||||
|         if (obj.int_value) pbf.writeVarintField(4, obj.int_value) | ||||
|         if (obj.uint_value) pbf.writeVarintField(5, obj.uint_value) | ||||
|         if (obj.sint_value) pbf.writeSVarintField(6, obj.sint_value) | ||||
|         if (obj.bool_value) pbf.writeBooleanField(7, obj.bool_value) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| class Tile { | ||||
|     // code generated by pbf v3.2.1
 | ||||
| 
 | ||||
|     static GeomType = { | ||||
|         UNKNOWN: { | ||||
|             value: 0, | ||||
|             options: {}, | ||||
|         }, | ||||
|         POINT: { | ||||
|             value: 1, | ||||
|             options: {}, | ||||
|         }, | ||||
|         LINESTRING: { | ||||
|             value: 2, | ||||
|             options: {}, | ||||
|         }, | ||||
|         POLYGON: { | ||||
|             value: 3, | ||||
|             options: {}, | ||||
|         }, | ||||
|     } | ||||
| 
 | ||||
|     public static read(pbf, end) { | ||||
|         return pbf.readFields(Tile._readField, { layers: [] }, end) | ||||
|     } | ||||
| 
 | ||||
|     static _readField(tag, obj, pbf) { | ||||
|         if (tag === 3) obj.layers.push(Layer.read(pbf, pbf.readVarint() + pbf.pos)) | ||||
|     } | ||||
| 
 | ||||
|     static write(obj, pbf) { | ||||
|         if (obj.layers) | ||||
|             for (var i = 0; i < obj.layers.length; i++) | ||||
|                 pbf.writeMessage(3, Layer.write, obj.layers[i]) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export default class MvtSource implements FeatureSourceForTile, UpdatableFeatureSource { | ||||
|     public readonly features: Store<GeojsonFeature<Geometry, { [name: string]: any }>[]> | ||||
|  | @ -352,7 +28,7 @@ export default class MvtSource implements FeatureSourceForTile, UpdatableFeature | |||
|         y: number, | ||||
|         z: number, | ||||
|         layerName?: string, | ||||
|         isActive?: Store<boolean> | ||||
|         isActive?: Store<boolean>, | ||||
|     ) { | ||||
|         this._url = url | ||||
|         this._layerName = layerName | ||||
|  | @ -367,7 +43,7 @@ export default class MvtSource implements FeatureSourceForTile, UpdatableFeature | |||
|                 } | ||||
|                 return fs | ||||
|             }, | ||||
|             [isActive] | ||||
|             [isActive], | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|  | @ -378,39 +54,6 @@ export default class MvtSource implements FeatureSourceForTile, UpdatableFeature | |||
|         await this.currentlyRunning | ||||
|     } | ||||
| 
 | ||||
|     private getValue(v: { | ||||
|         // Exactly one of these values must be present in a valid message
 | ||||
|         string_value?: string | ||||
|         float_value?: number | ||||
|         double_value?: number | ||||
|         int_value?: number | ||||
|         uint_value?: number | ||||
|         sint_value?: number | ||||
|         bool_value?: boolean | ||||
|     }): string | number | undefined | boolean { | ||||
|         if (v.string_value !== "") { | ||||
|             return v.string_value | ||||
|         } | ||||
|         if (v.double_value !== 0) { | ||||
|             return v.double_value | ||||
|         } | ||||
|         if (v.float_value !== 0) { | ||||
|             return v.float_value | ||||
|         } | ||||
|         if (v.int_value !== 0) { | ||||
|             return v.int_value | ||||
|         } | ||||
|         if (v.uint_value !== 0) { | ||||
|             return v.uint_value | ||||
|         } | ||||
|         if (v.sint_value !== 0) { | ||||
|             return v.sint_value | ||||
|         } | ||||
|         if (v.bool_value !== false) { | ||||
|             return v.bool_value | ||||
|         } | ||||
|         return undefined | ||||
|     } | ||||
| 
 | ||||
|     private async download(): Promise<void> { | ||||
|         try { | ||||
|  | @ -420,24 +63,27 @@ export default class MvtSource implements FeatureSourceForTile, UpdatableFeature | |||
|                 return | ||||
|             } | ||||
|             const buffer = await result.arrayBuffer() | ||||
|             const data = Tile.read(new Pbf(buffer), undefined) | ||||
|             const layers = data.layers | ||||
|             let layer = data.layers[0] | ||||
|             if (layers.length > 1) { | ||||
|                 if (!this._layerName) { | ||||
|                     throw "Multiple layers in the downloaded tile, but no layername is given to choose from" | ||||
|             const features = MvtToGeojson.fromBuffer(buffer, this.x, this.y, this.z) | ||||
|             for (const feature of features) { | ||||
|                 const properties = feature.properties | ||||
|                 if(!properties["osm_type"]){ | ||||
|                     continue | ||||
|                 } | ||||
|                 layer = layers.find((l) => l.name === this._layerName) | ||||
|             } | ||||
|             if (!layer) { | ||||
|                 return | ||||
|             } | ||||
|             const builder = new MvtFeatureBuilder(layer.extent, this.x, this.y, this.z) | ||||
|             const features: GeojsonFeature[] = [] | ||||
| 
 | ||||
|             for (const feature of layer.features) { | ||||
|                 const properties = this.inflateProperties(feature.tags, layer.keys, layer.values) | ||||
|                 features.push(builder.toGeoJson(feature.geometry, feature.type, properties)) | ||||
|                 let type: string = "node" | ||||
|                 switch (properties["osm_type"]) { | ||||
|                     case "N": | ||||
|                         type = "node" | ||||
|                         break | ||||
|                     case "W": | ||||
|                         type = "way" | ||||
|                         break | ||||
|                     case "R": | ||||
|                         type = "relation" | ||||
|                         break | ||||
|                 } | ||||
|                 properties["id"] = type + "/" + properties["osm_id"] | ||||
|                 delete properties["osm_id"] | ||||
|                 delete properties["osm_type"] | ||||
|             } | ||||
|             this._features.setData(features) | ||||
|         } catch (e) { | ||||
|  | @ -445,27 +91,5 @@ export default class MvtSource implements FeatureSourceForTile, UpdatableFeature | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private inflateProperties(tags: number[], keys: string[], values: { string_value: string }[]) { | ||||
|         const properties = {} | ||||
|         for (let i = 0; i < tags.length; i += 2) { | ||||
|             properties[keys[tags[i]]] = this.getValue(values[tags[i + 1]]) | ||||
|         } | ||||
|         let type: string | ||||
|         switch (properties["osm_type"]) { | ||||
|             case "N": | ||||
|                 type = "node" | ||||
|                 break | ||||
|             case "W": | ||||
|                 type = "way" | ||||
|                 break | ||||
|             case "R": | ||||
|                 type = "relation" | ||||
|                 break | ||||
|         } | ||||
|         properties["id"] = type + "/" + properties["osm_id"] | ||||
|         delete properties["osm_id"] | ||||
|         delete properties["osm_type"] | ||||
| 
 | ||||
|         return properties | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -92,6 +92,13 @@ export class GeoOperations { | |||
|         return turf.distance(lonlat0, lonlat1, { units: "meters" }) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Starting on `from`, travels `distance` meters in the direction of the `bearing` (default: 90) | ||||
|      */ | ||||
|     static destination(from: Coord | [number,number],distance: number, bearing: number = 90): [number,number]{ | ||||
|         return <[number,number]> turf.destination(from, distance, bearing, {units: "meters"}).geometry.coordinates | ||||
|     } | ||||
| 
 | ||||
|     static convexHull(featureCollection, options: { concavity?: number }) { | ||||
|         return turf.convex(featureCollection, options) | ||||
|     } | ||||
|  |  | |||
|  | @ -17,7 +17,8 @@ export interface ProvidedImage { | |||
|      */ | ||||
|     rotation?: number | ||||
|     lat?: number, | ||||
|     lon?: number | ||||
|     lon?: number, | ||||
|     host?: string | ||||
| } | ||||
| 
 | ||||
| export default abstract class ImageProvider { | ||||
|  | @ -25,7 +26,7 @@ export default abstract class ImageProvider { | |||
| 
 | ||||
|     public abstract readonly name: string | ||||
| 
 | ||||
|     public abstract SourceIcon(id?: string, location?: { lon: number; lat: number }): BaseUIElement | ||||
|     public abstract SourceIcon(img?: {id: string, url: string, host?: string}, location?: { lon: number; lat: number }): BaseUIElement | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|  |  | |||
|  | @ -118,13 +118,14 @@ export class Mapillary extends ImageProvider { | |||
|     } | ||||
| 
 | ||||
|     SourceIcon( | ||||
|         id: string, | ||||
|         img:  {id: string, url: string}, | ||||
|         location?: { | ||||
|             lon: number | ||||
|             lat: number | ||||
|         } | ||||
|     ): BaseUIElement { | ||||
|         let url: string = undefined | ||||
|         const id = img.id | ||||
|         if (id) { | ||||
|             url = Mapillary.createLink(location, 16, "" + id) | ||||
|         } | ||||
|  |  | |||
|  | @ -1,13 +1,15 @@ | |||
| import { ImageUploader } from "./ImageUploader" | ||||
| import { AuthorizedPanoramax, PanoramaxXYZ, ImageData } from "panoramax-js/dist" | ||||
| import { AuthorizedPanoramax, ImageData, Panoramax, PanoramaxXYZ } from "panoramax-js/dist" | ||||
| import ExifReader from "exifreader" | ||||
| import ImageProvider, { ProvidedImage } from "./ImageProvider" | ||||
| import BaseUIElement from "../../UI/BaseUIElement" | ||||
| import { LicenseInfo } from "./LicenseInfo" | ||||
| import { Utils } from "../../Utils" | ||||
| import { GeoOperations } from "../GeoOperations" | ||||
| import Constants from "../../Models/Constants" | ||||
| import { Store, Stores, UIEventSource } from "../UIEventSource" | ||||
| import SvelteUIElement from "../../UI/Base/SvelteUIElement" | ||||
| import Panoramax_bw from "../../assets/svg/Panoramax_bw.svelte" | ||||
| import Link from "../../UI/Base/Link" | ||||
| 
 | ||||
| 
 | ||||
| export default class PanoramaxImageProvider extends ImageProvider { | ||||
|  | @ -15,13 +17,18 @@ export default class PanoramaxImageProvider extends ImageProvider { | |||
|     public static readonly singleton = new PanoramaxImageProvider() | ||||
|     private static readonly xyz = new PanoramaxXYZ() | ||||
|     private static defaultPanoramax = new AuthorizedPanoramax(Constants.panoramax.url, Constants.panoramax.token) | ||||
| 
 | ||||
|     public defaultKeyPrefixes: string[] = ["panoramax"] | ||||
|     public readonly name: string = "panoramax" | ||||
| 
 | ||||
|     private static knownMeta: Record<string, { data: ImageData, time: Date }> = {} | ||||
| 
 | ||||
|     public SourceIcon(id?: string, location?: { lon: number; lat: number; }): BaseUIElement { | ||||
|         return undefined | ||||
|     public SourceIcon(img?: { id: string, url: string, host?: string }, location?: { lon: number; lat: number; }): BaseUIElement { | ||||
|         const p = new Panoramax(img.host) | ||||
|         return new Link(new SvelteUIElement(Panoramax_bw), p.createViewLink({ | ||||
|             imageId: img?.id, | ||||
|             location | ||||
|         }), true) | ||||
|     } | ||||
| 
 | ||||
|     public addKnownMeta(meta: ImageData) { | ||||
|  | @ -36,7 +43,7 @@ export default class PanoramaxImageProvider extends ImageProvider { | |||
|     private async getInfoFromMapComplete(id: string): Promise<{ data: ImageData, url: string }> { | ||||
|         const sequence = "6e702976-580b-419c-8fb3-cf7bd364e6f8" // We always reuse this sequence
 | ||||
|         const url = `https://panoramax.mapcomplete.org/` | ||||
|         const data = await PanoramaxImageProvider.defaultPanoramax.imageInfo(sequence, id) | ||||
|         const data = await PanoramaxImageProvider.defaultPanoramax.imageInfo(id, sequence) | ||||
|         return { url, data } | ||||
|     } | ||||
| 
 | ||||
|  | @ -68,10 +75,14 @@ export default class PanoramaxImageProvider extends ImageProvider { | |||
|         } | ||||
| 
 | ||||
|         const [lon, lat] = GeoOperations.centerpointCoordinates(meta) | ||||
|         const hd = meta.properties | ||||
|         console.log(">>>",meta) | ||||
|         // const hdUrl = new URL(hd)
 | ||||
|         return <ProvidedImage>{ | ||||
|             id: meta.id, | ||||
|             url: makeAbsolute(meta.assets.sd.href), | ||||
|             url_hd: makeAbsolute(meta.assets.hd.href), | ||||
|             host: meta["links"].find(l => l.rel === "root")?.href, | ||||
|             lon, lat, | ||||
|             key: "panoramax", | ||||
|             provider: this, | ||||
|  | @ -87,9 +98,9 @@ export default class PanoramaxImageProvider extends ImageProvider { | |||
|         } | ||||
|         const cached = PanoramaxImageProvider.knownMeta[id] | ||||
|         if (cached) { | ||||
|             if(new Date().getTime() - cached.time.getTime() < 1000){ | ||||
|             if (new Date().getTime() - cached.time.getTime() < 1000) { | ||||
| 
 | ||||
|             return { data: cached.data, url: undefined } | ||||
|                 return { data: cached.data, url: undefined } | ||||
|             } | ||||
|         } | ||||
|         try { | ||||
|  | @ -100,13 +111,15 @@ export default class PanoramaxImageProvider extends ImageProvider { | |||
|         try { | ||||
|             return await this.getInfoFromXYZ(id) | ||||
|         } catch (e) { | ||||
|                 console.debug(e) | ||||
|             console.debug(e) | ||||
|         } | ||||
|         return undefined | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     public async ExtractUrls(key: string, value: string): Promise<ProvidedImage[]> { | ||||
|         if (!Panoramax.isId(value)) { | ||||
|             return undefined | ||||
|         } | ||||
|         return [await this.getInfoFor(value).then(r => this.featureToImage(<any>r))] | ||||
|     } | ||||
| 
 | ||||
|  | @ -115,7 +128,7 @@ export default class PanoramaxImageProvider extends ImageProvider { | |||
|         const source = UIEventSource.FromPromise(super.getRelevantUrlsFor(tags, prefixes)) | ||||
| 
 | ||||
|         function hasLoading(data: ProvidedImage[]) { | ||||
|             if(data === undefined){ | ||||
|             if (data === undefined) { | ||||
|                 return true | ||||
|             } | ||||
|             return data?.some(img => img?.status !== undefined && img?.status !== "ready" && img?.status !== "broken") | ||||
|  |  | |||
|  | @ -10,6 +10,7 @@ import { Point } from "geojson" | |||
| import MvtSource from "../FeatureSource/Sources/MvtSource" | ||||
| import AllImageProviders from "../ImageProviders/AllImageProviders" | ||||
| import { Imgur } from "../ImageProviders/Imgur" | ||||
| import { Panoramax, PanoramaxXYZ } from "panoramax-js/dist" | ||||
| 
 | ||||
| interface ImageFetcher { | ||||
|     /** | ||||
|  | @ -102,7 +103,7 @@ class P4CImageFetcher implements ImageFetcher { | |||
|                 { | ||||
|                     mindate: new Date().getTime() - maxAgeSeconds, | ||||
|                     towardscenter: false, | ||||
|                 } | ||||
|                 }, | ||||
|             ) | ||||
|         } catch (e) { | ||||
|             console.log("P4C image fetcher failed with", e) | ||||
|  | @ -163,6 +164,55 @@ class ImagesInLoadedDataFetcher implements ImageFetcher { | |||
|     } | ||||
| } | ||||
| 
 | ||||
| class ImagesFromPanoramaxFetcher implements ImageFetcher { | ||||
|     private readonly _radius: number | ||||
|     private readonly _panoramax: Panoramax | ||||
|     name: string = "panoramax" | ||||
| 
 | ||||
|     constructor(url?: string, radius: number = 100) { | ||||
|         this._radius = radius | ||||
|         if (url) { | ||||
| 
 | ||||
|             this._panoramax = new Panoramax(url) | ||||
|         } else { | ||||
|             this._panoramax = new PanoramaxXYZ() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     public async fetchImages(lat: number, lon: number): Promise<P4CPicture[]> { | ||||
| 
 | ||||
|         const bboxObj = new BBox([ | ||||
|             GeoOperations.destination([lon, lat], this._radius * Math.sqrt(2), -45), | ||||
|             GeoOperations.destination([lon, lat], this._radius * Math.sqrt(2), 135), | ||||
|         ]) | ||||
|         const bbox: [number, number, number, number] = bboxObj.toLngLatFlat() | ||||
|         const images = await this._panoramax.search({ bbox, limit: 1000 }) | ||||
| 
 | ||||
|         return images.map(i => { | ||||
|             const [lng, lat] = i.geometry.coordinates | ||||
|             return ({ | ||||
|                 pictureUrl: i.assets.sd.href, | ||||
|                 coordinates: { lng, lat }, | ||||
| 
 | ||||
|                 provider: "panoramax", | ||||
|                 direction: i.properties["view:azimuth"], | ||||
|                 osmTags: { | ||||
|                     "panoramax": i.id, | ||||
|                 }, | ||||
|                 thumbUrl: i.assets.thumb.href, | ||||
|                 date: new Date(i.properties.datetime).getTime(), | ||||
|                 license: i.properties["geovisio:license"], | ||||
|                 author: i.providers.at(-1).name, | ||||
|                 detailsUrl: i.id, | ||||
|                 details: { | ||||
|                     isSpherical: i.properties["exif"]["Xmp.GPano.ProjectionType"] === "equirectangular", | ||||
|                 }, | ||||
|             }) | ||||
|         }) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| class ImagesFromCacheServerFetcher implements ImageFetcher { | ||||
|     private readonly _searchRadius: number | ||||
|     public readonly name = "fromCacheServer" | ||||
|  | @ -186,7 +236,7 @@ class ImagesFromCacheServerFetcher implements ImageFetcher { | |||
|     async fetchImagesForType( | ||||
|         targetlat: number, | ||||
|         targetlon: number, | ||||
|         type: "lines" | "pois" | "polygons" | ||||
|         type: "lines" | "pois" | "polygons", | ||||
|     ): Promise<P4CPicture[]> { | ||||
|         const { x, y, z } = Tiles.embedded_tile(targetlat, targetlon, 14) | ||||
| 
 | ||||
|  | @ -203,7 +253,7 @@ class ImagesFromCacheServerFetcher implements ImageFetcher { | |||
|                 }), | ||||
|                 x, | ||||
|                 y, | ||||
|                 z | ||||
|                 z, | ||||
|             ) | ||||
|             await src.updateAsync() | ||||
|             return src.features.data | ||||
|  | @ -360,6 +410,8 @@ export class CombinedFetcher { | |||
|         this.sources = [ | ||||
|             new ImagesInLoadedDataFetcher(indexedFeatures, radius), | ||||
|             new ImagesFromCacheServerFetcher(radius), | ||||
|             new ImagesFromPanoramaxFetcher(), | ||||
|             new ImagesFromPanoramaxFetcher(Constants.panoramax.url), | ||||
|             new MapillaryFetcher({ | ||||
|                 panoramas: "no", | ||||
|                 max_images: 25, | ||||
|  | @ -375,7 +427,7 @@ export class CombinedFetcher { | |||
|         lat: number, | ||||
|         lon: number, | ||||
|         state: UIEventSource<Record<string, "loading" | "done" | "error">>, | ||||
|         sink: UIEventSource<P4CPicture[]> | ||||
|         sink: UIEventSource<P4CPicture[]>, | ||||
|     ): Promise<void> { | ||||
|         try { | ||||
|             const pics = await source.fetchImages(lat, lon) | ||||
|  | @ -408,7 +460,7 @@ export class CombinedFetcher { | |||
| 
 | ||||
|     public getImagesAround( | ||||
|         lon: number, | ||||
|         lat: number | ||||
|         lat: number, | ||||
|     ): { | ||||
|         images: Store<P4CPicture[]> | ||||
|         state: Store<Record<string, "loading" | "done" | "error">> | ||||
|  |  | |||
|  | @ -49,6 +49,7 @@ | |||
|   import SidebarUnit from "../Base/SidebarUnit.svelte" | ||||
|   import Squares2x2 from "@babeard/svelte-heroicons/mini/Squares2x2" | ||||
|   import EnvelopeOpen from "@babeard/svelte-heroicons/mini/EnvelopeOpen" | ||||
|   import PanoramaxLink from "./PanoramaxLink.svelte" | ||||
| 
 | ||||
|   export let state: ThemeViewState | ||||
|   let userdetails = state.osmConnection.userDetails | ||||
|  | @ -232,6 +233,7 @@ | |||
|     <If condition={featureSwitches.featureSwitchEnableLogin}> | ||||
|       <OpenIdEditor mapProperties={state.mapProperties} /> | ||||
|       <OpenJosm {state} /> | ||||
|       <PanoramaxLink large={false} mapProperties={state.mapProperties} /> | ||||
|       <MapillaryLink large={false} mapProperties={state.mapProperties} /> | ||||
|     </If> | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										40
									
								
								src/UI/BigComponents/PanoramaxLink.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/UI/BigComponents/PanoramaxLink.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,40 @@ | |||
| <script lang="ts"> | ||||
|   import Translations from "../i18n/Translations" | ||||
|   import { Store } from "../../Logic/UIEventSource" | ||||
|   import Tr from "../Base/Tr.svelte" | ||||
|   import Mapillary_black from "../../assets/svg/Mapillary_black.svelte" | ||||
|   import { twMerge } from "tailwind-merge" | ||||
|   import { PanoramaxXYZ, Panoramax } from "panoramax-js/dist" | ||||
|   import Panoramax_bw from "../../assets/svg/Panoramax_bw.svelte" | ||||
|   import {default as Panoramax_svg} from "../../assets/svg/Panoramax.svelte" | ||||
| 
 | ||||
|   /* | ||||
|       A subtleButton which opens panoramax in a new tab at the current location | ||||
|        */ | ||||
| 
 | ||||
|   export let host: Panoramax = new PanoramaxXYZ() | ||||
|   export let mapProperties: { | ||||
|     readonly zoom: Store<number> | ||||
|     readonly location: Store<{ lon: number; lat: number }> | ||||
|   } | ||||
|   let location = mapProperties.location | ||||
|   let zoom = mapProperties.zoom | ||||
|   let href = location.mapD(location => | ||||
|     host.createViewLink({ | ||||
|       location, | ||||
|       zoom: zoom.data, | ||||
|     }), [zoom]) | ||||
|   export let large: boolean = true | ||||
| </script> | ||||
| 
 | ||||
| <a class="flex items-center" href={$href} target="_blank"> | ||||
|   <Panoramax_svg class={twMerge("shrink-0", large ? "m-2 mr-4 h-12 w-12" : "h-5 w-5 pr-1")} /> | ||||
|   {#if large} | ||||
|     <div class="flex flex-col"> | ||||
|       <Tr t={Translations.t.general.attribution.openPanoramax} /> | ||||
|       <Tr cls="subtle" t={Translations.t.general.attribution.panoramaxHelp} /> | ||||
|     </div> | ||||
|   {:else} | ||||
|     <Tr t={Translations.t.general.attribution.openPanoramax} /> | ||||
|   {/if} | ||||
| </a> | ||||
|  | @ -16,7 +16,7 @@ | |||
|   let license: Store<LicenseInfo> = UIEventSource.FromPromise( | ||||
|     image.provider?.DownloadAttribution(image) | ||||
|   ) | ||||
|   let icon = image.provider?.SourceIcon(image.id) | ||||
|   let icon = image.provider?.SourceIcon(image) | ||||
| </script> | ||||
| 
 | ||||
| {#if $license !== undefined} | ||||
|  |  | |||
|  | @ -130,7 +130,7 @@ | |||
|     for (const f of features) { | ||||
|       bbox = bbox.unionWith(BBox.get(f)) | ||||
|     } | ||||
|     mapProperties.maxbounds.set(bbox.pad(1.1)) | ||||
|     mapProperties.maxbounds.set(bbox.pad(4)) | ||||
|   }) | ||||
| 
 | ||||
|   ) | ||||
|  |  | |||
|  | @ -1511,7 +1511,6 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
|         if (!element) { | ||||
|             return | ||||
|         } | ||||
|         console.log("Scrolling into view:", element) | ||||
|         // Is the element completely in the view?
 | ||||
|         const parentRect = Utils.findParentWithScrolling(element)?.getBoundingClientRect() | ||||
|         if (!parentRect) { | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| <script> | ||||
| export let color = "#000000" | ||||
| </script> | ||||
|  <svg {...$$restProps} on:click on:mouseover on:mouseenter on:mouseleave on:keydown on:focus    width="375px"    height="375px"    viewBox="0 0 375 375"    version="1.1"    id="svg1"    sodipodi:docname="circle.svg"    inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)"    xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"    xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"    xmlns="http://www.w3.org/2000/svg"    xmlns:svg="http://www.w3.org/2000/svg">   <defs      id="defs1" />   <sodipodi:namedview      id="namedview1"      pagecolor="#505050"      bordercolor="#eeeeee"      borderopacity="1"      inkscape:showpageshadow="0"      inkscape:pageopacity="0"      inkscape:pagecheckerboard="0"      inkscape:deskcolor="#d1d1d1"      inkscape:zoom="2.056"      inkscape:cx="187.5"      inkscape:cy="187.5"      inkscape:window-width="1920"      inkscape:window-height="995"      inkscape:window-x="0"      inkscape:window-y="0"      inkscape:window-maximized="1"      inkscape:current-layer="svg1" />   <path      style="fill:{color}"      class="selectable"      d="M 375,187.5 C 375,291.05469 291.05469,375 187.5,375 83.945312,375 0,291.05469 0,187.5 0,83.945312 83.945312,0 187.5,0 291.05469,0 375,83.945312 375,187.5 Z m 0,0"      id="path1" /> </svg>  | ||||
|  <svg {...$$restProps} on:click on:mouseover on:mouseenter on:mouseleave on:keydown on:focus    width="375px"    height="375px"    viewBox="0 0 375 375"    version="1.1"    id="svg1"    sodipodi:docname="circle.svg"    inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)"    xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"    xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"    xmlns="http://www.w3.org/2000/svg"    xmlns:svg="http://www.w3.org/2000/svg">   <defs      id="defs1" />   <sodipodi:namedview      id="namedview1"      pagecolor="#505050"      bordercolor="#eeeeee"      borderopacity="1"      inkscape:showpageshadow="0"      inkscape:pageopacity="0"      inkscape:pagecheckerboard="0"      inkscape:deskcolor="#d1d1d1"      inkscape:zoom="2.056"      inkscape:cx="187.5"      inkscape:cy="187.5"      inkscape:window-width="1920"      inkscape:window-height="995"      inkscape:window-x="0"      inkscape:window-y="0"      inkscape:window-maximized="1"      inkscape:current-layer="svg1" />   <path      style="fill:{color};"      class="selectable"      d="M 375,187.5 C 375,291.05469 291.05469,375 187.5,375 83.945312,375 0,291.05469 0,187.5 0,83.945312 83.945312,0 187.5,0 291.05469,0 375,83.945312 375,187.5 Z m 0,0"      id="path1" /> </svg>  | ||||
							
								
								
									
										4
									
								
								src/assets/svg/Panoramax.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/assets/svg/Panoramax.svelte
									
										
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										4
									
								
								src/assets/svg/Panoramax_bw.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/assets/svg/Panoramax_bw.svelte
									
										
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue