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