diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6784f0169..2e6dfab28 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. See [standa
+
+
### [0.47.2](https://github.com/pietervdvn/mapcomplete/compare/v0.47.1...v0.47.2) (2024-09-24)
@@ -64,6 +66,25 @@ All notable changes to this project will be documented in this file. See [standa
* **waste:** add filter for 'recycling centre' ([5da63bf](https://github.com/pietervdvn/MapComplete/commit/5da63bf83aa7d8b230c8dbc082be3fba33344289))
+### [0.46.12](https://github.com/USERNAME/REPOSITORY_NAME/compare/v0.46.11...v0.46.12) (2024-09-29)
+
+
+### Features
+
+* more integrations of panoramax: add panoramax to nearby images view, add to drawer, add icon, see [#1451](https://github.com/pietervdvn/MapComplete/issues/1451) ([5fd1d75](https://github.com/USERNAME/REPOSITORY_NAME/commits5fd1d75efb0fbd49cac3eb5f7a37104887f92f10))
+
+
+### Bug Fixes
+
+* add panoramax.xyz to CSP list ([89d31f5](https://github.com/USERNAME/REPOSITORY_NAME/commits89d31f54bcd9f7f2bf62a663d67aa5bf1308307a))
+* load external panoramax images ([df9c44a](https://github.com/USERNAME/REPOSITORY_NAME/commitsdf9c44aaed7b2670134e8f9128343022e92c6e8d))
+
+
+### Theme improvements
+
+* **drinking water:** fix [#2184](https://github.com/pietervdvn/MapComplete/issues/2184) ([308d2ba](https://github.com/USERNAME/REPOSITORY_NAME/commits308d2ba4c62cc66d9c30cd1536549411c0abcb88))
+
+
### [0.46.11](https://github.com/USERNAME/REPOSITORY_NAME/compare/v0.46.10...v0.46.11) (2024-09-28)
diff --git a/assets/layers/drinking_water/drinking_water.json b/assets/layers/drinking_water/drinking_water.json
index f41776588..ed40d8dbf 100644
--- a/assets/layers/drinking_water/drinking_water.json
+++ b/assets/layers/drinking_water/drinking_water.json
@@ -177,9 +177,13 @@
},
"mappings": [
{
- "if": "operational_status=",
+ "if": {
+ "and": [
+ "operational_status=",
+ "disused:amenity="
+ ]
+ },
"addExtraTags": [
- "disused:amenity=",
"amenity=drinking_water"
],
"then": {
@@ -221,6 +225,15 @@
"ca": "Aquesta font d'aigua potable està tancada",
"cs": "Tato pitná voda je uzavřena"
}
+ },
+ {
+ "if": "disused:amenity=drinking_water",
+ "addExtraTags": [
+ "amenity="
+ ],
+ "then": {
+ "en": "This drinking water is permanently closed"
+ }
}
],
"id": "Still in use?"
diff --git a/assets/svg/license_info.json b/assets/svg/license_info.json
index 8e80ddef7..b5712434e 100644
--- a/assets/svg/license_info.json
+++ b/assets/svg/license_info.json
@@ -887,6 +887,26 @@
"https://www.OpenStreetMap.org"
]
},
+ {
+ "path": "panoramax.svg",
+ "license": "LOGO",
+ "authors": [
+ "Panoramax"
+ ],
+ "sources": [
+ "https://commons.wikimedia.org/wiki/File:Panoramax.svg"
+ ]
+ },
+ {
+ "path": "panoramax_bw.svg",
+ "license": "LOGO",
+ "authors": [
+ "Panoramax"
+ ],
+ "sources": [
+ "https://commons.wikimedia.org/wiki/File:Panoramax.svg"
+ ]
+ },
{
"path": "party.svg",
"license": "CC-BY-4.0",
diff --git a/assets/svg/panoramax.svg b/assets/svg/panoramax.svg
new file mode 100644
index 000000000..ed1746c88
--- /dev/null
+++ b/assets/svg/panoramax.svg
@@ -0,0 +1,187 @@
+
+
+
+image/svg+xml
diff --git a/assets/svg/panoramax.svg.license b/assets/svg/panoramax.svg.license
new file mode 100644
index 000000000..f0263b4cb
--- /dev/null
+++ b/assets/svg/panoramax.svg.license
@@ -0,0 +1,2 @@
+SPDX-FileCopyrightText: Panoramax
+SPDX-License-Identifier: LicenseRef-LOGO
\ No newline at end of file
diff --git a/assets/svg/panoramax_bw.svg b/assets/svg/panoramax_bw.svg
new file mode 100644
index 000000000..e50264ff3
--- /dev/null
+++ b/assets/svg/panoramax_bw.svg
@@ -0,0 +1,217 @@
+
+
+
+image/svg+xml
+
+
+
+
+
+
diff --git a/assets/svg/panoramax_bw.svg.license b/assets/svg/panoramax_bw.svg.license
new file mode 100644
index 000000000..f0263b4cb
--- /dev/null
+++ b/assets/svg/panoramax_bw.svg.license
@@ -0,0 +1,2 @@
+SPDX-FileCopyrightText: Panoramax
+SPDX-License-Identifier: LicenseRef-LOGO
\ No newline at end of file
diff --git a/langs/en.json b/langs/en.json
index dcc97ecc7..70823edba 100644
--- a/langs/en.json
+++ b/langs/en.json
@@ -203,7 +203,9 @@
"openMapillary": "Open Mapillary here",
"openOsmcha": "See latest edits made with {theme}",
"openOsmchaLastWeek": "See edits from the last 7 days",
+ "openPanoramax": "Open Panoramax here",
"openThemeDocumentation": "Open the documentation for thematic map {name}",
+ "panoramaxHelp": "Panoramax is an online service which gathers street-level pictures and offers them under a free license. Contributors are allowed to use these pictures to improve OpenStreetMap",
"seeOnMapillary": "See this image on Mapillary",
"themeBy": "Theme maintained by {author}",
"title": "Copyright and attribution",
diff --git a/langs/layers/en.json b/langs/layers/en.json
index d39ceb361..b09b9de5d 100644
--- a/langs/layers/en.json
+++ b/langs/layers/en.json
@@ -4214,6 +4214,9 @@
},
"2": {
"then": "This drinking water is closed"
+ },
+ "3": {
+ "then": "This drinking water is permanently closed"
}
},
"question": "Is this drinking water spot still operational?",
diff --git a/package-lock.json b/package-lock.json
index 8137640bd..94ebd094b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -58,15 +58,15 @@
"maplibre-gl": "^4.1.1",
"marked": "^12.0.2",
"monaco-editor": "^0.46.0",
+ "mvt-to-geojson": "^0.0.5",
"name-suggestion-index": "^6.0.20240422",
"npm": "^10.7.0",
"opening_hours": "^3.6.0",
"osm-auth": "^2.5.0",
"osmtogeojson": "^3.0.0-beta.5",
- "panoramax-js": "^0.1.4",
+ "panoramax-js": "^0.3.6",
"panzoom": "^9.4.3",
"papaparse": "^5.3.1",
- "pbf": "^3.2.1",
"pg": "^8.11.3",
"pic4carto": "^2.1.15",
"pmtiles": "^3.0.5",
@@ -13144,6 +13144,15 @@
"version": "1.0.0",
"license": "MIT"
},
+ "node_modules/mvt-to-geojson": {
+ "version": "0.0.5",
+ "resolved": "https://registry.npmjs.org/mvt-to-geojson/-/mvt-to-geojson-0.0.5.tgz",
+ "integrity": "sha512-IG4bJJP9nxY/9LenWBY0yi5SEv9a1G06trP5b4Tg01LCrzB32US/FO9bygJr7c+ZVbt175AzBwJiiawaxfLTbA==",
+ "dependencies": {
+ "@types/geojson": "^7946.0.14",
+ "pbf": "^3.2.1"
+ }
+ },
"node_modules/n3": {
"version": "1.17.3",
"license": "MIT",
@@ -15993,9 +16002,9 @@
"license": "MIT"
},
"node_modules/panoramax-js": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/panoramax-js/-/panoramax-js-0.1.4.tgz",
- "integrity": "sha512-X7plFMH1ndxiiyVFEluDloNiEBH0nEkurCPJ7zAInxbgv21pp/EGFwu3ynmF5ETyyXB9zu0n309juyjTdJ5pnQ==",
+ "version": "0.3.6",
+ "resolved": "https://registry.npmjs.org/panoramax-js/-/panoramax-js-0.3.6.tgz",
+ "integrity": "sha512-CRdXwh91H6chd1PYptG070ukx+S6IkVaUeQVG91ySevoJoCYOuBT65qkMhRo49X2um1nGcs9UqolW90R57875g==",
"dependencies": {
"@ogcapi-js/features": "^1.1.1",
"@ogcapi-js/shared": "^1.1.1",
@@ -30244,6 +30253,15 @@
"murmurhash-js": {
"version": "1.0.0"
},
+ "mvt-to-geojson": {
+ "version": "0.0.5",
+ "resolved": "https://registry.npmjs.org/mvt-to-geojson/-/mvt-to-geojson-0.0.5.tgz",
+ "integrity": "sha512-IG4bJJP9nxY/9LenWBY0yi5SEv9a1G06trP5b4Tg01LCrzB32US/FO9bygJr7c+ZVbt175AzBwJiiawaxfLTbA==",
+ "requires": {
+ "@types/geojson": "^7946.0.14",
+ "pbf": "^3.2.1"
+ }
+ },
"n3": {
"version": "1.17.3",
"requires": {
@@ -32069,9 +32087,9 @@
"version": "1.0.0"
},
"panoramax-js": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/panoramax-js/-/panoramax-js-0.1.4.tgz",
- "integrity": "sha512-X7plFMH1ndxiiyVFEluDloNiEBH0nEkurCPJ7zAInxbgv21pp/EGFwu3ynmF5ETyyXB9zu0n309juyjTdJ5pnQ==",
+ "version": "0.3.6",
+ "resolved": "https://registry.npmjs.org/panoramax-js/-/panoramax-js-0.3.6.tgz",
+ "integrity": "sha512-CRdXwh91H6chd1PYptG070ukx+S6IkVaUeQVG91ySevoJoCYOuBT65qkMhRo49X2um1nGcs9UqolW90R57875g==",
"requires": {
"@ogcapi-js/features": "^1.1.1",
"@ogcapi-js/shared": "^1.1.1",
diff --git a/package.json b/package.json
index 404541dac..4ab761cd4 100644
--- a/package.json
+++ b/package.json
@@ -203,15 +203,15 @@
"maplibre-gl": "^4.1.1",
"marked": "^12.0.2",
"monaco-editor": "^0.46.0",
+ "mvt-to-geojson": "^0.0.5",
"name-suggestion-index": "^6.0.20240422",
"npm": "^10.7.0",
"opening_hours": "^3.6.0",
"osm-auth": "^2.5.0",
"osmtogeojson": "^3.0.0-beta.5",
- "panoramax-js": "^0.1.4",
+ "panoramax-js": "^0.3.6",
"panzoom": "^9.4.3",
"papaparse": "^5.3.1",
- "pbf": "^3.2.1",
"pg": "^8.11.3",
"pic4carto": "^2.1.15",
"pmtiles": "^3.0.5",
diff --git a/scripts/generateLayouts.ts b/scripts/generateLayouts.ts
index 7313546d7..fb57fc760 100644
--- a/scripts/generateLayouts.ts
+++ b/scripts/generateLayouts.ts
@@ -330,6 +330,8 @@ class GenerateLayouts extends Script {
"https://www.openstreetmap.org",
"https://api.openstreetmap.org",
"https://pietervdvn.goatcounter.com",
+ "https://api.panoramax.xyz",
+ "https://panoramax.mapcomplete.org"
].concat(...(await this.eliUrls()))
SpecialVisualizations.specialVisualizations.forEach((sv) => {
diff --git a/src/Logic/BBox.ts b/src/Logic/BBox.ts
index c12357e26..a5fa3c46a 100644
--- a/src/Logic/BBox.ts
+++ b/src/Logic/BBox.ts
@@ -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({})
diff --git a/src/Logic/FeatureSource/Sources/MvtSource.ts b/src/Logic/FeatureSource/Sources/MvtSource.ts
index 1605ba177..6342fea90 100644
--- a/src/Logic/FeatureSource/Sources/MvtSource.ts
+++ b/src/Logic/FeatureSource/Sources/MvtSource.ts
@@ -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(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: type,
- coordinates: 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[]>
@@ -352,7 +28,7 @@ export default class MvtSource implements FeatureSourceForTile, UpdatableFeature
y: number,
z: number,
layerName?: string,
- isActive?: Store
+ isActive?: Store,
) {
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 {
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
- }
}
diff --git a/src/Logic/GeoOperations.ts b/src/Logic/GeoOperations.ts
index 51423fc29..96062012a 100644
--- a/src/Logic/GeoOperations.ts
+++ b/src/Logic/GeoOperations.ts
@@ -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)
}
diff --git a/src/Logic/ImageProviders/ImageProvider.ts b/src/Logic/ImageProviders/ImageProvider.ts
index 50878a94a..4588297c8 100644
--- a/src/Logic/ImageProviders/ImageProvider.ts
+++ b/src/Logic/ImageProviders/ImageProvider.ts
@@ -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
/**
diff --git a/src/Logic/ImageProviders/Mapillary.ts b/src/Logic/ImageProviders/Mapillary.ts
index 5a26e31c2..2cc370f32 100644
--- a/src/Logic/ImageProviders/Mapillary.ts
+++ b/src/Logic/ImageProviders/Mapillary.ts
@@ -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)
}
diff --git a/src/Logic/ImageProviders/Panoramax.ts b/src/Logic/ImageProviders/Panoramax.ts
index eb73b9636..0fc4da425 100644
--- a/src/Logic/ImageProviders/Panoramax.ts
+++ b/src/Logic/ImageProviders/Panoramax.ts
@@ -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 = {}
- 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 {
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 {
+ if (!Panoramax.isId(value)) {
+ return undefined
+ }
return [await this.getInfoFor(value).then(r => this.featureToImage(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")
diff --git a/src/Logic/Web/NearbyImagesSearch.ts b/src/Logic/Web/NearbyImagesSearch.ts
index 49853485d..bf9f571f4 100644
--- a/src/Logic/Web/NearbyImagesSearch.ts
+++ b/src/Logic/Web/NearbyImagesSearch.ts
@@ -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 {
+
+ 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 {
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>,
- sink: UIEventSource
+ sink: UIEventSource,
): Promise {
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
state: Store>
diff --git a/src/UI/BigComponents/MenuDrawer.svelte b/src/UI/BigComponents/MenuDrawer.svelte
index 902fbe8b2..593069b4a 100644
--- a/src/UI/BigComponents/MenuDrawer.svelte
+++ b/src/UI/BigComponents/MenuDrawer.svelte
@@ -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 @@
+
diff --git a/src/UI/BigComponents/PanoramaxLink.svelte b/src/UI/BigComponents/PanoramaxLink.svelte
new file mode 100644
index 000000000..058674c06
--- /dev/null
+++ b/src/UI/BigComponents/PanoramaxLink.svelte
@@ -0,0 +1,40 @@
+
+
+
+
+ {#if large}
+
+
+
+
+ {:else}
+
+ {/if}
+
diff --git a/src/UI/Image/ImageAttribution.svelte b/src/UI/Image/ImageAttribution.svelte
index 9594c87d8..46bb525fc 100644
--- a/src/UI/Image/ImageAttribution.svelte
+++ b/src/UI/Image/ImageAttribution.svelte
@@ -16,7 +16,7 @@
let license: Store = UIEventSource.FromPromise(
image.provider?.DownloadAttribution(image)
)
- let icon = image.provider?.SourceIcon(image.id)
+ let icon = image.provider?.SourceIcon(image)
{#if $license !== undefined}
diff --git a/src/UI/Image/NearbyImages.svelte b/src/UI/Image/NearbyImages.svelte
index d70dc2ba5..ca9f3ceb8 100644
--- a/src/UI/Image/NearbyImages.svelte
+++ b/src/UI/Image/NearbyImages.svelte
@@ -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))
})
)
diff --git a/src/Utils.ts b/src/Utils.ts
index 386b3ee02..37f6c8e9a 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -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) {
diff --git a/src/assets/svg/Circle.svelte b/src/assets/svg/Circle.svelte
index f1926471a..510ddce25 100644
--- a/src/assets/svg/Circle.svelte
+++ b/src/assets/svg/Circle.svelte
@@ -1,4 +1,4 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/src/assets/svg/Panoramax.svelte b/src/assets/svg/Panoramax.svelte
new file mode 100644
index 000000000..6a20baf15
--- /dev/null
+++ b/src/assets/svg/Panoramax.svelte
@@ -0,0 +1,4 @@
+
+ image/svg+xml
\ No newline at end of file
diff --git a/src/assets/svg/Panoramax_bw.svelte b/src/assets/svg/Panoramax_bw.svelte
new file mode 100644
index 000000000..71900d7f0
--- /dev/null
+++ b/src/assets/svg/Panoramax_bw.svelte
@@ -0,0 +1,4 @@
+
+ image/svg+xml
\ No newline at end of file