forked from MapComplete/MapComplete
		
	Scripts: more fixes to the uploadPanoramaxScript
This commit is contained in:
		
							parent
							
								
									664ddc686c
								
							
						
					
					
						commit
						6e335c91d4
					
				
					 5 changed files with 275 additions and 93 deletions
				
			
		| 
						 | 
					@ -138,7 +138,7 @@
 | 
				
			||||||
    "generate:summaryCache": "vite-node scripts/generateSummaryTileCache.ts",
 | 
					    "generate:summaryCache": "vite-node scripts/generateSummaryTileCache.ts",
 | 
				
			||||||
    "create:database": "vite-node scripts/osm2pgsql/createNewDatabase.ts",
 | 
					    "create:database": "vite-node scripts/osm2pgsql/createNewDatabase.ts",
 | 
				
			||||||
    "delete:database:old": "vite-node scripts/osm2pgsql/deleteOldDbs.ts",
 | 
					    "delete:database:old": "vite-node scripts/osm2pgsql/deleteOldDbs.ts",
 | 
				
			||||||
    "upload:panoramax": "vite-node scripts/ImgurToPanoramax.ts && josm imgur_to_panoramax.osc"
 | 
					    "upload:panoramax": "vite-node scripts/ImgurToPanoramax.ts # && josm imgur_to_panoramax.osc"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "keywords": [
 | 
					  "keywords": [
 | 
				
			||||||
    "OpenStreetMap",
 | 
					    "OpenStreetMap",
 | 
				
			||||||
| 
						 | 
					@ -209,7 +209,7 @@
 | 
				
			||||||
    "opening_hours": "^3.6.0",
 | 
					    "opening_hours": "^3.6.0",
 | 
				
			||||||
    "osm-auth": "^2.5.0",
 | 
					    "osm-auth": "^2.5.0",
 | 
				
			||||||
    "osmtogeojson": "^3.0.0-beta.5",
 | 
					    "osmtogeojson": "^3.0.0-beta.5",
 | 
				
			||||||
    "panoramax-js": "^0.4.8",
 | 
					    "panoramax-js": "^0.3.12",
 | 
				
			||||||
    "panzoom": "^9.4.3",
 | 
					    "panzoom": "^9.4.3",
 | 
				
			||||||
    "papaparse": "^5.3.1",
 | 
					    "papaparse": "^5.3.1",
 | 
				
			||||||
    "pg": "^8.11.3",
 | 
					    "pg": "^8.11.3",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,7 +4,7 @@ import { RegexTag } from "../src/Logic/Tags/RegexTag"
 | 
				
			||||||
import Constants from "../src/Models/Constants"
 | 
					import Constants from "../src/Models/Constants"
 | 
				
			||||||
import { BBox } from "../src/Logic/BBox"
 | 
					import { BBox } from "../src/Logic/BBox"
 | 
				
			||||||
import { existsSync, readFileSync, writeFileSync } from "fs"
 | 
					import { existsSync, readFileSync, writeFileSync } from "fs"
 | 
				
			||||||
import { PanoramaxUploader } from "../src/Logic/ImageProviders/Panoramax"
 | 
					import PanoramaxImageProvider, { PanoramaxUploader } from "../src/Logic/ImageProviders/Panoramax"
 | 
				
			||||||
import { Feature } from "geojson"
 | 
					import { Feature } from "geojson"
 | 
				
			||||||
import { LicenseInfo } from "../src/Logic/ImageProviders/LicenseInfo"
 | 
					import { LicenseInfo } from "../src/Logic/ImageProviders/LicenseInfo"
 | 
				
			||||||
import { GeoOperations } from "../src/Logic/GeoOperations"
 | 
					import { GeoOperations } from "../src/Logic/GeoOperations"
 | 
				
			||||||
| 
						 | 
					@ -16,27 +16,31 @@ import { Changes } from "../src/Logic/Osm/Changes"
 | 
				
			||||||
import { ChangeDescription } from "../src/Logic/Osm/Actions/ChangeDescription"
 | 
					import { ChangeDescription } from "../src/Logic/Osm/Actions/ChangeDescription"
 | 
				
			||||||
import OsmObjectDownloader from "../src/Logic/Osm/OsmObjectDownloader"
 | 
					import OsmObjectDownloader from "../src/Logic/Osm/OsmObjectDownloader"
 | 
				
			||||||
import { OsmObject } from "../src/Logic/Osm/OsmObject"
 | 
					import { OsmObject } from "../src/Logic/Osm/OsmObject"
 | 
				
			||||||
import { createReadStream } from "node:fs"
 | 
					 | 
				
			||||||
import { File } from "buffer"
 | 
					import { File } from "buffer"
 | 
				
			||||||
import { open } from "node:fs/promises"
 | 
					import { open } from "node:fs/promises"
 | 
				
			||||||
import { UploadableTag } from "../src/Logic/Tags/TagTypes"
 | 
					import { UploadableTag } from "../src/Logic/Tags/TagTypes"
 | 
				
			||||||
 | 
					import { Imgur } from "../src/Logic/ImageProviders/Imgur"
 | 
				
			||||||
 | 
					import { Or } from "../src/Logic/Tags/Or"
 | 
				
			||||||
 | 
					import ScriptUtils from "./ScriptUtils"
 | 
				
			||||||
 | 
					import { ImmutableStore } from "../src/Logic/UIEventSource"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class ImgurToPanoramax extends Script {
 | 
					export class ImgurToPanoramax extends Script {
 | 
				
			||||||
    private readonly panoramax = new PanoramaxUploader(
 | 
					    private readonly panoramax = new PanoramaxUploader(
 | 
				
			||||||
        Constants.panoramax.url,
 | 
					        Constants.panoramax.url,
 | 
				
			||||||
        Constants.panoramax.token
 | 
					        Constants.panoramax.token
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					    private licenseChecker = new PanoramaxImageProvider()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private readonly alreadyUploaded: Record<string, string> = {}
 | 
					    private readonly alreadyUploaded: Record<string, string> = this.readAlreadyUploaded()
 | 
				
			||||||
 | 
					    private readonly alreadyUploadedInv: Record<string, string> = Utils.transposeMapSimple(this.alreadyUploaded)
 | 
				
			||||||
    private _imageDirectory: string
 | 
					    private _imageDirectory: string
 | 
				
			||||||
    private _licenseDirectory: string
 | 
					    private _licenseDirectory: string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private readonly sequenceIds = {
 | 
					    private readonly sequenceIds = {
 | 
				
			||||||
        test: "7f34cf53-27ff-46c9-ac22-78511fa8457a",
 | 
					        test: "7f34cf53-27ff-46c9-ac22-78511fa8457a",
 | 
				
			||||||
        cc0: "f0d6f78a-ff95-4db1-8494-6eb44a17bb37",
 | 
					        cc0: "1de6f4a1-73ac-4c75-ab7f-2a2aabddf50a", // "f0d6f78a-ff95-4db1-8494-6eb44a17bb37",
 | 
				
			||||||
        ccby: "288a8052-b475-422c-811a-4f6f1a00015e",
 | 
					        ccby: "288a8052-b475-422c-811a-4f6f1a00015e",
 | 
				
			||||||
        ccbysa: "f3d02893-b4c1-4cd6-8b27-e27ab57eb59a",
 | 
					        ccbysa: "f3d02893-b4c1-4cd6-8b27-e27ab57eb59a"
 | 
				
			||||||
    } as const
 | 
					    } as const
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    constructor() {
 | 
					    constructor() {
 | 
				
			||||||
| 
						 | 
					@ -45,23 +49,92 @@ export class ImgurToPanoramax extends Script {
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private async getRawInfo(imgurUrl): Promise<{ description?: string, datetime: number }> {
 | 
				
			||||||
 | 
					        const fallbackpath = this._licenseDirectory + "/raw/" + imgurUrl.replaceAll(/[^a-zA-Z0-9]/g, "_") + ".json"
 | 
				
			||||||
 | 
					        if (existsSync(fallbackpath)) {
 | 
				
			||||||
 | 
					            console.log("Loaded raw info from fallback path")
 | 
				
			||||||
 | 
					            return JSON.parse(readFileSync(fallbackpath, "utf8"))["data"]
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        // No local data available; lets ask imgur themselves
 | 
				
			||||||
 | 
					        return new Promise((resolve) => {
 | 
				
			||||||
 | 
					            Imgur.singleton.DownloadAttribution({ url: imgurUrl },
 | 
				
			||||||
 | 
					                raw => {
 | 
				
			||||||
 | 
					                    console.log("Writing fallback to", fallbackpath, "(via raw)")
 | 
				
			||||||
 | 
					                    writeFileSync(fallbackpath, JSON.stringify(raw), "utf8")
 | 
				
			||||||
 | 
					                    resolve(raw["data"])
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private async getLicenseFor(imgurUrl: string): Promise<LicenseInfo> {
 | 
				
			||||||
 | 
					        const imageName = imgurUrl.split("/").at(-1)
 | 
				
			||||||
 | 
					        const licensePath: string = this._licenseDirectory + "/" + imageName
 | 
				
			||||||
 | 
					        if (existsSync(licensePath)) {
 | 
				
			||||||
 | 
					            const rawText = readFileSync(licensePath, "utf8")
 | 
				
			||||||
 | 
					            if (rawText?.toLowerCase() === "cc0" || rawText?.toLowerCase().startsWith("cc0")) {
 | 
				
			||||||
 | 
					                return { licenseShortName: "CC0", artist: "Unknown" }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                const licenseText: LicenseInfo = JSON.parse(rawText)
 | 
				
			||||||
 | 
					                if (licenseText.licenseShortName) {
 | 
				
			||||||
 | 
					                    return licenseText
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                console.log("<<< No valid license found in text", rawText)
 | 
				
			||||||
 | 
					                return undefined
 | 
				
			||||||
 | 
					            } catch (e) {
 | 
				
			||||||
 | 
					                console.error("Could not read ", rawText.slice(0, 20), "as json for image", imgurUrl, "from", licensePath)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // We didn't find the expected license in the expected location; search for the fallback (raw) license
 | 
				
			||||||
 | 
					        const fallbackpath = this._licenseDirectory + "/raw/" + imgurUrl.replaceAll(/[^a-zA-Z0-9]/g, "_") + ".json"
 | 
				
			||||||
 | 
					        if (existsSync(fallbackpath)) {
 | 
				
			||||||
 | 
					            const fallbackRaw: string = JSON.parse(readFileSync(fallbackpath, "utf8"))["data"]?.description
 | 
				
			||||||
 | 
					            if (fallbackRaw?.toLowerCase()?.startsWith("cc0") || fallbackRaw?.toLowerCase()?.indexOf("#cc0") >= 0) {
 | 
				
			||||||
 | 
					                return { licenseShortName: "CC0", artist: "Unknown" }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					           const license = Imgur.parseLicense(fallbackRaw)
 | 
				
			||||||
 | 
					            if(license){
 | 
				
			||||||
 | 
					                return license
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            console.log("No (fallback) license found for (but file exists), not uploading", imgurUrl, fallbackRaw)
 | 
				
			||||||
 | 
					            return undefined
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // No local data available; lets ask imgur themselves
 | 
				
			||||||
 | 
					        const attr = await Imgur.singleton.DownloadAttribution({ url: imgurUrl },
 | 
				
			||||||
 | 
					            raw => {
 | 
				
			||||||
 | 
					                console.log("Writing fallback to", fallbackpath)
 | 
				
			||||||
 | 
					                writeFileSync(fallbackpath, JSON.stringify(raw), "utf8")
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					        console.log("Got license via API:", attr.licenseShortName)
 | 
				
			||||||
 | 
					        await ScriptUtils.sleep(500)
 | 
				
			||||||
 | 
					        if (attr.licenseShortName) {
 | 
				
			||||||
 | 
					            return attr
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return undefined
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async uploadImage(
 | 
					    async uploadImage(
 | 
				
			||||||
        key: string,
 | 
					        key: string,
 | 
				
			||||||
        feat: Feature,
 | 
					        feat: Feature
 | 
				
			||||||
        sequences: {
 | 
					 | 
				
			||||||
            id: string
 | 
					 | 
				
			||||||
            "stats:items": { count: number }
 | 
					 | 
				
			||||||
        }[]
 | 
					 | 
				
			||||||
    ): Promise<UploadableTag | undefined> {
 | 
					    ): Promise<UploadableTag | undefined> {
 | 
				
			||||||
        const v = feat.properties[key]
 | 
					        const v = feat.properties[key]
 | 
				
			||||||
        if (!v) {
 | 
					        if (!v) {
 | 
				
			||||||
            return undefined
 | 
					            return undefined
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        const imageHash = v.split("/").at(-1).split(".").at(0)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (this.alreadyUploaded[imageHash]) {
 | 
					        const imageHash = v.split("/").at(-1).split(".").at(0)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
            const panohash = this.alreadyUploaded[imageHash]
 | 
					            const panohash = this.alreadyUploaded[imageHash]
 | 
				
			||||||
            return new And([new Tag(key.replace("image", panohash), panohash), new Tag(key, "")])
 | 
					            if (panohash) {
 | 
				
			||||||
 | 
					                console.log("Already uploaded", panohash)
 | 
				
			||||||
 | 
					                return new And([new Tag(key.replace("image", "panoramax"), panohash), new Tag(key, "")])
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let path: string = undefined
 | 
					        let path: string = undefined
 | 
				
			||||||
| 
						 | 
					@ -73,22 +146,12 @@ export class ImgurToPanoramax extends Script {
 | 
				
			||||||
        if (!path) {
 | 
					        if (!path) {
 | 
				
			||||||
            return undefined
 | 
					            return undefined
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        const licensePath =
 | 
					        const license: LicenseInfo = await this.getLicenseFor(v)
 | 
				
			||||||
            this._licenseDirectory + "/" + v.replaceAll(/[^a-zA-Z0-9]/g, "_") + ".json"
 | 
					        if (license === undefined) {
 | 
				
			||||||
        if (!existsSync(licensePath)) {
 | 
					 | 
				
			||||||
            return undefined
 | 
					            return undefined
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        const licenseText: LicenseInfo = JSON.parse(readFileSync(licensePath, "utf8"))
 | 
					        const sequence = this.sequenceIds[license.licenseShortName?.toLowerCase()]
 | 
				
			||||||
        if (!licenseText.licenseShortName) {
 | 
					 | 
				
			||||||
            console.log("No license found for", path, licenseText)
 | 
					 | 
				
			||||||
            return undefined
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        const license = licenseText.licenseShortName.toLowerCase().split(" ")[0].replace(/-/g, "")
 | 
					 | 
				
			||||||
        const sequence = this.sequenceIds[license]
 | 
					 | 
				
			||||||
        const author = licenseText.artist
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const handle = await open(path)
 | 
					        const handle = await open(path)
 | 
				
			||||||
 | 
					 | 
				
			||||||
        const stat = await handle.stat()
 | 
					        const stat = await handle.stat()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        class MyFile extends File {
 | 
					        class MyFile extends File {
 | 
				
			||||||
| 
						 | 
					@ -104,71 +167,148 @@ export class ImgurToPanoramax extends Script {
 | 
				
			||||||
            return handle.readableWebStream()
 | 
					            return handle.readableWebStream()
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const licenseRaw = await this.getRawInfo(v)
 | 
				
			||||||
 | 
					        const date = new Date(licenseRaw.datetime * 1000)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        console.log("Uploading", imageHash, sequence)
 | 
					        console.log("Uploading", imageHash, sequence)
 | 
				
			||||||
        const result = await this.panoramax.uploadImage(
 | 
					        const result = await this.panoramax.uploadImage(
 | 
				
			||||||
            <any>file,
 | 
					            <any>file,
 | 
				
			||||||
            GeoOperations.centerpointCoordinates(feat),
 | 
					            GeoOperations.centerpointCoordinates(feat),
 | 
				
			||||||
            author,
 | 
					            license.artist,
 | 
				
			||||||
            true,
 | 
					            true,
 | 
				
			||||||
            sequence
 | 
					            sequence,
 | 
				
			||||||
 | 
					            date.toISOString()
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        this.alreadyUploaded[imageHash] = result.value
 | 
					 | 
				
			||||||
        await handle.close()
 | 
					        await handle.close()
 | 
				
			||||||
 | 
					        this.alreadyUploaded[imageHash] = result.value
 | 
				
			||||||
 | 
					        this.writeAlreadyUploaded()
 | 
				
			||||||
        return new And([new Tag(key.replace("image", result.key), result.value), new Tag(key, "")])
 | 
					        return new And([new Tag(key.replace("image", result.key), result.value), new Tag(key, "")])
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private writeAlreadyUploaded() {
 | 
				
			||||||
 | 
					        writeFileSync("uploaded_images.json", JSON.stringify(this.alreadyUploaded))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private readAlreadyUploaded() {
 | 
				
			||||||
 | 
					        const uploaded = JSON.parse(readFileSync("uploaded_images.json", "utf8"))
 | 
				
			||||||
 | 
					        console.log("Detected ", Object.keys(uploaded).length, "previously uploaded images")
 | 
				
			||||||
 | 
					        return uploaded
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private async patchDate(panokey: string) {
 | 
				
			||||||
 | 
					        const imgurkey = this.alreadyUploadedInv[panokey]
 | 
				
			||||||
 | 
					        const license = await this.getRawInfo("https://i.imgur.com/" + imgurkey + ".jpg")
 | 
				
			||||||
 | 
					        const date = new Date(license.datetime * 1000)
 | 
				
			||||||
 | 
					        const panolicense = await this.panoramax.panoramax.search({
 | 
				
			||||||
 | 
					            ids: [panokey]
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        const panodata = panolicense[0]
 | 
				
			||||||
 | 
					        const collection: string = panodata.collection
 | 
				
			||||||
 | 
					        console.log({ imgurkey, date, panodata, datetime: license.datetime })
 | 
				
			||||||
 | 
					        const p = this.panoramax.panoramax
 | 
				
			||||||
 | 
					        const url = p.host+"/collections/" + collection + "/items/" + panokey
 | 
				
			||||||
 | 
					        const result = await p.fetch(url, {
 | 
				
			||||||
 | 
					            method: "PATCH",
 | 
				
			||||||
 | 
					            headers: { "content-type": "application/json" },
 | 
				
			||||||
 | 
					            body: JSON.stringify({
 | 
				
			||||||
 | 
					                ts: date.getTime(),
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        console.log("Patched date of ", p.createViewLink({
 | 
				
			||||||
 | 
					            imageId: panokey,
 | 
				
			||||||
 | 
					        }), url, "result is", result.status, await result.text())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async main(args: string[]): Promise<void> {
 | 
					    async main(args: string[]): Promise<void> {
 | 
				
			||||||
        this._imageDirectory = args[0] ?? "/home/pietervdvn/data/imgur-image-backup"
 | 
					        this._imageDirectory = args[0] ?? "/home/pietervdvn/data/imgur-image-backup"
 | 
				
			||||||
        this._licenseDirectory = args[1] ?? "/home/pietervdvn/git/MapComplete-data/ImageLicenseInfo"
 | 
					        this._licenseDirectory = args[1] ?? "/home/pietervdvn/git/MapComplete-data/ImageLicenseInfo"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const bounds = new BBox([
 | 
					      //  await this.panoramax.panoramax.createCollection("CC0 - part 2")
 | 
				
			||||||
            [3.6984301050112833, 51.06715570450848],
 | 
					      //  return
 | 
				
			||||||
            [3.7434328399847914, 51.039379568816145],
 | 
					      /*  for (const panohash in this.alreadyUploadedInv) {
 | 
				
			||||||
        ])
 | 
					            await this.patchDate(panohash)
 | 
				
			||||||
        const maxcount = 500
 | 
					            break
 | 
				
			||||||
        const filter = new RegexTag("image", /^https:\/\/i.imgur.com\/.*/)
 | 
					        }*/
 | 
				
			||||||
        const overpass = new Overpass(filter, [], Constants.defaultOverpassUrls[0])
 | 
					 | 
				
			||||||
        const features = (await overpass.queryGeoJson(bounds))[0].features
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const bounds = new BBox([
 | 
				
			||||||
 | 
					            [
 | 
				
			||||||
 | 
					                1.7217767788980893,
 | 
				
			||||||
 | 
					                41.00219164121438
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            [
 | 
				
			||||||
 | 
					                2.7238939148245436,
 | 
				
			||||||
 | 
					                41.9258679932085
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					        ])
 | 
				
			||||||
 | 
					        const maxcount = 10000
 | 
				
			||||||
 | 
					        const overpassfilters: RegexTag[] = []
 | 
				
			||||||
 | 
					        const r = /^https:\/\/i.imgur.com\/.*/
 | 
				
			||||||
 | 
					        for (const k of ["image", "image:menu", "image:streetsign"]) {
 | 
				
			||||||
 | 
					            overpassfilters.push(new RegexTag(k, r))
 | 
				
			||||||
 | 
					            for (let i = 0; i < 20; i++) {
 | 
				
			||||||
 | 
					                overpassfilters.push(new RegexTag(k + ":" + i, r))
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        const overpass = new Overpass(new Or(overpassfilters), [], Constants.defaultOverpassUrls[0], new ImmutableStore(500) )
 | 
				
			||||||
 | 
					        const features = (await overpass.queryGeoJson(bounds))[0].features
 | 
				
			||||||
 | 
					        const featuresCopy = [...features]
 | 
				
			||||||
        let converted = 0
 | 
					        let converted = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const pano = this.panoramax.panoramax
 | 
					        const total = features.length
 | 
				
			||||||
        const sequences = await pano.mySequences()
 | 
					 | 
				
			||||||
        const changes: ChangeDescription[] = []
 | 
					        const changes: ChangeDescription[] = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        do {
 | 
					        do {
 | 
				
			||||||
            const f = features.shift()
 | 
					            const f = features.shift()
 | 
				
			||||||
            if (!f) {
 | 
					            if (!f) {
 | 
				
			||||||
                break
 | 
					                break
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					            if (converted % 100 === 0) {
 | 
				
			||||||
 | 
					                console.log("Converted:", converted, "total:", total, "progress:", Math.round(converted * 100 / total) + "%")
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const changedTags: (UploadableTag | undefined)[] = []
 | 
					            let changedTags: (UploadableTag | undefined)[] = []
 | 
				
			||||||
 | 
					            console.log("Handling "+f.properties.id)
 | 
				
			||||||
            for (const k of ["image", "image:menu", "image:streetsign"]) {
 | 
					            for (const k of ["image", "image:menu", "image:streetsign"]) {
 | 
				
			||||||
                changedTags.push(await this.uploadImage(k, f, sequences))
 | 
					                changedTags.push(await this.uploadImage(k, f))
 | 
				
			||||||
                for (let i = 0; i < 20; i++) {
 | 
					                for (let i = 0; i < 20; i++) {
 | 
				
			||||||
                    changedTags.push(await this.uploadImage(k + ":" + i, f, sequences))
 | 
					                    changedTags.push(await this.uploadImage(k + ":" + i, f))
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					            changedTags = Utils.NoNull(changedTags)
 | 
				
			||||||
 | 
					            if (changedTags.length > 0) {
 | 
				
			||||||
                const action = new ChangeTagAction(
 | 
					                const action = new ChangeTagAction(
 | 
				
			||||||
                    f.properties.id,
 | 
					                    f.properties.id,
 | 
				
			||||||
                new And(Utils.NoNull(changedTags)),
 | 
					                    new And(changedTags),
 | 
				
			||||||
                    f.properties,
 | 
					                    f.properties,
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        theme: "image-mover",
 | 
					                        theme: "image-mover",
 | 
				
			||||||
                    changeType: "link-image",
 | 
					                        changeType: "link-image"
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                changes.push(...(await action.CreateChangeDescriptions()))
 | 
					                changes.push(...(await action.CreateChangeDescriptions()))
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
            converted++
 | 
					            converted++
 | 
				
			||||||
        } while (converted < maxcount)
 | 
					        } while (converted < maxcount)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        console.log("Uploaded images for", converted, "items; now creating the changeset")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const modif: string[] = Utils.Dedup(changes.map((ch) => ch.type + "/" + ch.id))
 | 
					        const modif: string[] = Utils.Dedup(changes.map((ch) => ch.type + "/" + ch.id))
 | 
				
			||||||
        const modifiedObjectsFresh = <OsmObject[]>(
 | 
					        const modifiedObjectsFresh: OsmObject[] = []
 | 
				
			||||||
            (
 | 
					        const dloader = new OsmObjectDownloader()
 | 
				
			||||||
                await Promise.all(
 | 
					        for (let i = 0; i < modif.length; i++) {
 | 
				
			||||||
                    modif.map((id) => new OsmObjectDownloader().DownloadObjectAsync(id))
 | 
					            if (i % 100 === 0) {
 | 
				
			||||||
                )
 | 
					                console.log("Downloaded osm object", i, "/", modif.length, "(" + Math.round(i * 100 / modif.length) + "%)")
 | 
				
			||||||
            ).filter((m) => m !== "deleted")
 | 
					            }
 | 
				
			||||||
        )
 | 
					            const id = modif[i]
 | 
				
			||||||
 | 
					            const obj = await dloader.DownloadObjectAsync(id)
 | 
				
			||||||
 | 
					            if (obj === "deleted") {
 | 
				
			||||||
 | 
					                continue
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            modifiedObjectsFresh.push(obj)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        const modifiedObjects = Changes.createChangesetObjectsStatic(
 | 
					        const modifiedObjects = Changes.createChangesetObjectsStatic(
 | 
				
			||||||
            changes,
 | 
					            changes,
 | 
				
			||||||
            modifiedObjectsFresh,
 | 
					            modifiedObjectsFresh,
 | 
				
			||||||
| 
						 | 
					@ -177,6 +317,14 @@ export class ImgurToPanoramax extends Script {
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        const cs = Changes.buildChangesetXML("0", modifiedObjects)
 | 
					        const cs = Changes.buildChangesetXML("0", modifiedObjects)
 | 
				
			||||||
        writeFileSync("imgur_to_panoramax.osc", cs, "utf8")
 | 
					        writeFileSync("imgur_to_panoramax.osc", cs, "utf8")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const usernames = featuresCopy.map(f => f.properties.user)
 | 
				
			||||||
 | 
					        const hist : Record<string,number> = {}
 | 
				
			||||||
 | 
					        for (const username of usernames) {
 | 
				
			||||||
 | 
					            hist[username] = (hist[username] ?? 0)+ 1
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        console.log(hist)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,6 +11,7 @@ export class Imgur extends ImageProvider {
 | 
				
			||||||
    public readonly defaultKeyPrefixes: string[] = ["image"]
 | 
					    public readonly defaultKeyPrefixes: string[] = ["image"]
 | 
				
			||||||
    public static readonly apiUrl = "https://api.imgur.com/3/image"
 | 
					    public static readonly apiUrl = "https://api.imgur.com/3/image"
 | 
				
			||||||
    public static readonly supportingUrls = ["https://i.imgur.com"]
 | 
					    public static readonly supportingUrls = ["https://i.imgur.com"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private constructor() {
 | 
					    private constructor() {
 | 
				
			||||||
        super()
 | 
					        super()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -30,13 +31,44 @@ export class Imgur extends ImageProvider {
 | 
				
			||||||
                    url: value,
 | 
					                    url: value,
 | 
				
			||||||
                    key: key,
 | 
					                    key: key,
 | 
				
			||||||
                    provider: this,
 | 
					                    provider: this,
 | 
				
			||||||
                    id: value,
 | 
					                    id: value
 | 
				
			||||||
                },
 | 
					                }
 | 
				
			||||||
            ]
 | 
					            ]
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return undefined
 | 
					        return undefined
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static parseLicense(descr: string) {
 | 
				
			||||||
 | 
					        const data: Record<string, string> = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!descr) {
 | 
				
			||||||
 | 
					            return undefined
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (descr.toLowerCase() === "cc0") {
 | 
				
			||||||
 | 
					            data.author = "Unknown"
 | 
				
			||||||
 | 
					            data.license = "CC0"
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            for (const tag of descr.split("\n")) {
 | 
				
			||||||
 | 
					                const kv = tag.split(":")
 | 
				
			||||||
 | 
					                if (kv.length < 2) {
 | 
				
			||||||
 | 
					                    continue
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                const k = kv[0]
 | 
				
			||||||
 | 
					                data[k] = kv[1]?.replace(/\r/g, "")
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (Object.keys(data).length === 0) {
 | 
				
			||||||
 | 
					            return undefined
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const licenseInfo = new LicenseInfo()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        licenseInfo.licenseShortName = data.license
 | 
				
			||||||
 | 
					        licenseInfo.artist = data.author
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return licenseInfo
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Download the attribution and license info for the picture at the given URL
 | 
					     * Download the attribution and license info for the picture at the given URL
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
| 
						 | 
					@ -56,7 +88,9 @@ export class Imgur extends ImageProvider {
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public async DownloadAttribution(providedImage: { url: string }): Promise<LicenseInfo> {
 | 
					    public async DownloadAttribution(providedImage: {
 | 
				
			||||||
 | 
					        url: string
 | 
				
			||||||
 | 
					    }, withResponse?: (obj) => void): Promise<LicenseInfo> {
 | 
				
			||||||
        const url = providedImage.url
 | 
					        const url = providedImage.url
 | 
				
			||||||
        const hash = url.substr("https://i.imgur.com/".length).split(/\.jpe?g/i)[0]
 | 
					        const hash = url.substr("https://i.imgur.com/".length).split(/\.jpe?g/i)[0]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -64,26 +98,17 @@ export class Imgur extends ImageProvider {
 | 
				
			||||||
        const response = await Utils.downloadJsonCached<{
 | 
					        const response = await Utils.downloadJsonCached<{
 | 
				
			||||||
            data: { description: string; datetime: string; views: number }
 | 
					            data: { description: string; datetime: string; views: number }
 | 
				
			||||||
        }>(apiUrl, 365 * 24 * 60 * 60, {
 | 
					        }>(apiUrl, 365 * 24 * 60 * 60, {
 | 
				
			||||||
            Authorization: "Client-ID " + Constants.ImgurApiKey,
 | 
					            Authorization: "Client-ID " + Constants.ImgurApiKey
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
 | 
					        if (withResponse) {
 | 
				
			||||||
 | 
					            withResponse(response)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const descr = response.data.description ?? ""
 | 
					 | 
				
			||||||
        const data: any = {}
 | 
					 | 
				
			||||||
        const imgurData = response.data
 | 
					        const imgurData = response.data
 | 
				
			||||||
 | 
					        const license= Imgur.parseLicense(imgurData.description ?? "")
 | 
				
			||||||
 | 
					        license.views = imgurData.views
 | 
				
			||||||
 | 
					        license.date = new Date(Number(imgurData.datetime) * 1000)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for (const tag of descr.split("\n")) {
 | 
					        return license
 | 
				
			||||||
            const kv = tag.split(":")
 | 
					 | 
				
			||||||
            const k = kv[0]
 | 
					 | 
				
			||||||
            data[k] = kv[1]?.replace(/\r/g, "")
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const licenseInfo = new LicenseInfo()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        licenseInfo.licenseShortName = data.license
 | 
					 | 
				
			||||||
        licenseInfo.artist = data.author
 | 
					 | 
				
			||||||
        licenseInfo.date = new Date(Number(imgurData.datetime) * 1000)
 | 
					 | 
				
			||||||
        licenseInfo.views = imgurData.views
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return licenseInfo
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -145,12 +145,7 @@ export default class PanoramaxImageProvider extends ImageProvider {
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Stores.Chronic(1500, () => hasLoading(source.data)).addCallback((_) => {
 | 
					        Stores.Chronic(1500, () => hasLoading(source.data)).addCallback(() => {
 | 
				
			||||||
            console.log(
 | 
					 | 
				
			||||||
                "Testing panoramax URLS again as some were loading",
 | 
					 | 
				
			||||||
                source.data,
 | 
					 | 
				
			||||||
                hasLoading(source.data)
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            super.getRelevantUrlsFor(tags, prefixes).then((data) => {
 | 
					            super.getRelevantUrlsFor(tags, prefixes).then((data) => {
 | 
				
			||||||
                source.set(data)
 | 
					                source.set(data)
 | 
				
			||||||
                return !hasLoading(data)
 | 
					                return !hasLoading(data)
 | 
				
			||||||
| 
						 | 
					@ -192,9 +187,9 @@ export default class PanoramaxImageProvider extends ImageProvider {
 | 
				
			||||||
export class PanoramaxUploader implements ImageUploader {
 | 
					export class PanoramaxUploader implements ImageUploader {
 | 
				
			||||||
    public readonly panoramax: AuthorizedPanoramax
 | 
					    public readonly panoramax: AuthorizedPanoramax
 | 
				
			||||||
    maxFileSizeInMegabytes = 100 * 1000 * 1000 // 100MB
 | 
					    maxFileSizeInMegabytes = 100 * 1000 * 1000 // 100MB
 | 
				
			||||||
    private readonly _targetSequence: Store<string>
 | 
					    private readonly _targetSequence?: Store<string>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    constructor(url: string, token: string, targetSequence: Store<string>) {
 | 
					    constructor(url: string, token: string, targetSequence?: Store<string>) {
 | 
				
			||||||
        this._targetSequence = targetSequence
 | 
					        this._targetSequence = targetSequence
 | 
				
			||||||
        this.panoramax = new AuthorizedPanoramax(url, token)
 | 
					        this.panoramax = new AuthorizedPanoramax(url, token)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -204,7 +199,8 @@ export class PanoramaxUploader implements ImageUploader {
 | 
				
			||||||
        currentGps: [number, number],
 | 
					        currentGps: [number, number],
 | 
				
			||||||
        author: string,
 | 
					        author: string,
 | 
				
			||||||
        noblur: boolean = false,
 | 
					        noblur: boolean = false,
 | 
				
			||||||
        sequenceId?: string
 | 
					        sequenceId?: string,
 | 
				
			||||||
 | 
					        datetime?: string
 | 
				
			||||||
    ): Promise<{
 | 
					    ): Promise<{
 | 
				
			||||||
        key: string
 | 
					        key: string
 | 
				
			||||||
        value: string
 | 
					        value: string
 | 
				
			||||||
| 
						 | 
					@ -213,7 +209,7 @@ export class PanoramaxUploader implements ImageUploader {
 | 
				
			||||||
        // https://panoramax.openstreetmap.fr/api/docs/swagger#/
 | 
					        // https://panoramax.openstreetmap.fr/api/docs/swagger#/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let [lon, lat] = currentGps
 | 
					        let [lon, lat] = currentGps
 | 
				
			||||||
        let datetime = new Date().toISOString()
 | 
					        datetime ??= new Date().toISOString()
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            const tags = await ExifReader.load(blob)
 | 
					            const tags = await ExifReader.load(blob)
 | 
				
			||||||
            const [[latD], [latM], [latS, latSDenom]] = <
 | 
					            const [[latD], [latM], [latS, latSDenom]] = <
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										13
									
								
								src/Utils.ts
									
										
									
									
									
								
							
							
						
						
									
										13
									
								
								src/Utils.ts
									
										
									
									
									
								
							| 
						 | 
					@ -1291,6 +1291,19 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
 | 
				
			||||||
        return newD
 | 
					        return newD
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * {"a": "b", "c":"d"} // => {"b":"a", "d":"c"}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public static transposeMapSimple<K extends  string, V extends string>(d: Record<K, V>): Record<V, K>{
 | 
				
			||||||
 | 
					        const inv = <Record<V, K>> {}
 | 
				
			||||||
 | 
					        for (const k in d) {
 | 
				
			||||||
 | 
					            const v = d[k]
 | 
				
			||||||
 | 
					            inv[v] = k
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return inv
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Utils.colorAsHex({r: 255, g: 128, b: 0}) // => "#ff8000"
 | 
					     * Utils.colorAsHex({r: 255, g: 128, b: 0}) // => "#ff8000"
 | 
				
			||||||
     * Utils.colorAsHex(undefined) // => undefined
 | 
					     * Utils.colorAsHex(undefined) // => undefined
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue