forked from MapComplete/MapComplete
Refactoring: remove scripts and leaderboard which depend on imgur
This commit is contained in:
parent
e3a984a2dd
commit
d920afa09e
5 changed files with 0 additions and 566 deletions
|
@ -1,75 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta content="width=device-width, initial-scale=1.0, user-scalable=no" name="viewport">
|
|
||||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' https://gc.zgo.at/; img-src *; connect-src 'self' https://raw.githubusercontent.com/ https://www.openstreetmap.org/ https://api.openstreetmap.org/;">
|
|
||||||
<link href="./css/mobile.css" rel="stylesheet"/>
|
|
||||||
<link href="./css/openinghourstable.css" rel="stylesheet"/>
|
|
||||||
<link href="./css/tagrendering.css" rel="stylesheet"/>
|
|
||||||
<link href="css/ReviewElement.css" rel="stylesheet"/>
|
|
||||||
<link href="./css/index-tailwind-output.css" rel="stylesheet"/>
|
|
||||||
<link href="./css/wikipedia.css" rel="stylesheet"/>
|
|
||||||
<meta content="website" property="og:type">
|
|
||||||
|
|
||||||
<!-- THEME-SPECIFIC -->
|
|
||||||
<!-- Every theme gets their own html page, this is created by a script; this part will be removed except for the index -->
|
|
||||||
<title>MapComplete</title>
|
|
||||||
<link href="./index.webmanifest" rel="manifest">
|
|
||||||
|
|
||||||
<link href="./assets/svg/add.svg" rel="icon" sizes="any" type="image/svg+xml">
|
|
||||||
<meta content="./assets/SocialImage.png" property="og:image">
|
|
||||||
<meta content="MapComplete - editable, thematic maps with OpenStreetMap" property="og:title">
|
|
||||||
<meta content="MapComplete is a platform to visualize OpenStreetMap on a specific topic and to easily contribute data back to it."
|
|
||||||
property="og:description">
|
|
||||||
|
|
||||||
<link href="./assets/generated/images/assets_svg_mapcomplete_logo512.png" rel="apple-touch-icon" sizes="512x512">
|
|
||||||
<link href="./assets/generated/images/assets_svg_mapcomplete_logo384.png" rel="apple-touch-icon" sizes="384x384">
|
|
||||||
<link href="./assets/generated/images/assets_svg_mapcomplete_logo192.png" rel="apple-touch-icon" sizes="192x192">
|
|
||||||
<link href="./assets/generated/images/assets_svg_mapcomplete_logo180.png" rel="apple-touch-icon" sizes="180x180">
|
|
||||||
<link href="./assets/generated/images/assets_svg_mapcomplete_logo152.png" rel="apple-touch-icon" sizes="152x152">
|
|
||||||
<link href="./assets/generated/images/assets_svg_mapcomplete_logo144.png" rel="apple-touch-icon" sizes="144x144">
|
|
||||||
<link href="./assets/generated/images/assets_svg_mapcomplete_logo128.png" rel="apple-touch-icon" sizes="128x128">
|
|
||||||
<link href="./assets/generated/images/assets_svg_mapcomplete_logo120.png" rel="apple-touch-icon" sizes="120x120">
|
|
||||||
<link href="./assets/generated/images/assets_svg_mapcomplete_logo96.png" rel="apple-touch-icon" sizes="96x96">
|
|
||||||
<link href="./assets/generated/images/assets_svg_mapcomplete_logo72.png" rel="apple-touch-icon" sizes="72x72">
|
|
||||||
|
|
||||||
|
|
||||||
<!-- THEME-SPECIFIC-END-->
|
|
||||||
|
|
||||||
<style>
|
|
||||||
#decoration-desktop img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<!-- Mastodon link verification: https://docs.joinmastodon.org/user/profile/#Link%20verification -->
|
|
||||||
<a rel="me" href="https://en.osm.town/@MapComplete" class="hidden">Mastodon</a>
|
|
||||||
|
|
||||||
<div id="main"></div>
|
|
||||||
<script type="module" src="./src/leaderboard.ts"></script>
|
|
||||||
<script async data-goatcounter="https://pietervdvn.goatcounter.com/count" src="https://gc.zgo.at/count.js" crossorigin="anonymous"
|
|
||||||
integrity="sha384-QfJMxHNngbaF6IXH2kBwubsLYh7GVSFmJOX1O1YKJBq+zv1VVypB9BysTzyG1D1U"></script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
window.addEventListener('load', () => {
|
|
||||||
if ('serviceWorker' in navigator) {
|
|
||||||
navigator.serviceWorker.register('/service-worker.js').then(
|
|
||||||
() => {
|
|
||||||
console.log('Service worker registration successful');
|
|
||||||
},
|
|
||||||
err => {
|
|
||||||
console.error('Service worker registration failed', err)
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log("Service workers are not supported")
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -121,7 +121,6 @@
|
||||||
"download:nsi": "npm i name-suggestion-index && vite-node scripts/generateStats.ts && npm run download:nsi-logos && vite-node scripts/nsiLogos.ts -- all",
|
"download:nsi": "npm i name-suggestion-index && vite-node scripts/generateStats.ts && npm run download:nsi-logos && vite-node scripts/nsiLogos.ts -- all",
|
||||||
"download:editor-layer-index": "vite-node scripts/downloadEli.ts",
|
"download:editor-layer-index": "vite-node scripts/downloadEli.ts",
|
||||||
"download:stats": "vite-node scripts/GenerateSeries.ts",
|
"download:stats": "vite-node scripts/GenerateSeries.ts",
|
||||||
"download:images": "vite-node scripts/generateImageAnalysis.ts -- ~/data/imgur-image-backup/",
|
|
||||||
"download:community-index": "vite-node scripts/downloadCommunityIndex.ts ",
|
"download:community-index": "vite-node scripts/downloadCommunityIndex.ts ",
|
||||||
"weblate:add-upstream": "git remote add weblate https://translate.mapcomplete.org/git/mapcomplete/core/ ; git remote update weblate",
|
"weblate:add-upstream": "git remote add weblate https://translate.mapcomplete.org/git/mapcomplete/core/ ; git remote update weblate",
|
||||||
"weblate:fix": "npm run weblate:add-upstream && git merge weblate/master && git rebase origin/master && git push origin master",
|
"weblate:fix": "npm run weblate:add-upstream && git merge weblate/master && git rebase origin/master && git push origin master",
|
||||||
|
|
|
@ -1,414 +0,0 @@
|
||||||
import Script from "./Script"
|
|
||||||
import { Overpass } from "../src/Logic/Osm/Overpass"
|
|
||||||
import { RegexTag } from "../src/Logic/Tags/RegexTag"
|
|
||||||
import { ImmutableStore } from "../src/Logic/UIEventSource"
|
|
||||||
import { BBox } from "../src/Logic/BBox"
|
|
||||||
import * as fs from "fs"
|
|
||||||
import { writeFileSync } from "fs"
|
|
||||||
import { Feature } from "geojson"
|
|
||||||
import ScriptUtils from "./ScriptUtils"
|
|
||||||
import { Imgur } from "../src/Logic/ImageProviders/Imgur"
|
|
||||||
import { LicenseInfo } from "../src/Logic/ImageProviders/LicenseInfo"
|
|
||||||
import { Utils } from "../src/Utils"
|
|
||||||
import Constants from "../src/Models/Constants"
|
|
||||||
|
|
||||||
export default class GenerateImageAnalysis extends Script {
|
|
||||||
/**
|
|
||||||
* Max N in `image:N`-keys and `imageN` keys
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private static readonly maxImageIndex = 31
|
|
||||||
constructor() {
|
|
||||||
super(
|
|
||||||
[
|
|
||||||
"Downloads (from overpass) all tags which have an imgur-image; then analyses the licenses and downloads all the images",
|
|
||||||
"",
|
|
||||||
"Arguments:",
|
|
||||||
"Path to download the images to",
|
|
||||||
"Path to save the overview to",
|
|
||||||
].join("\n")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetchImages(key: string, datapath: string, refresh: boolean): Promise<void> {
|
|
||||||
const targetPath = `${datapath}/features_with_${key.replace(/[:\/]/, "_")}.geojson`
|
|
||||||
if (fs.existsSync(targetPath) && !refresh) {
|
|
||||||
console.log("Skipping", key)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const tag = new RegexTag(key, /^https:\/\/i.imgur.com\/.*$/i)
|
|
||||||
const overpass = new Overpass(
|
|
||||||
tag,
|
|
||||||
[],
|
|
||||||
Constants.defaultOverpassUrls[0], //"https://overpass.kumi.systems/api/interpreter",
|
|
||||||
new ImmutableStore(500),
|
|
||||||
false
|
|
||||||
)
|
|
||||||
console.log("Starting query...")
|
|
||||||
const data = await overpass.queryGeoJson(BBox.global)
|
|
||||||
console.log(
|
|
||||||
"Got data:",
|
|
||||||
data[0].features.length,
|
|
||||||
"items; timestamp:",
|
|
||||||
data[1].toISOString()
|
|
||||||
)
|
|
||||||
fs.writeFileSync(targetPath, JSON.stringify(data[0]), "utf8")
|
|
||||||
console.log("Written", targetPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
async downloadData(datapath: string, refresh: boolean): Promise<void> {
|
|
||||||
if (!fs.existsSync(datapath)) {
|
|
||||||
fs.mkdirSync(datapath)
|
|
||||||
}
|
|
||||||
await this.fetchImages("image", datapath, refresh)
|
|
||||||
await this.fetchImages("image:streetsign", datapath, refresh)
|
|
||||||
await this.fetchImages("image:menu", datapath, refresh)
|
|
||||||
for (let i = 0; i < GenerateImageAnalysis.maxImageIndex; i++) {
|
|
||||||
await this.fetchImages("image:" + i, datapath, refresh)
|
|
||||||
await this.fetchImages("image" + i, datapath, refresh)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadData(datapath: string): Feature[] {
|
|
||||||
const allFeatures: Feature[] = []
|
|
||||||
|
|
||||||
const files = ScriptUtils.readDirRecSync(datapath)
|
|
||||||
for (const file of files) {
|
|
||||||
if (!file.endsWith(".geojson")) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const contents = JSON.parse(fs.readFileSync(file, "utf8"))
|
|
||||||
allFeatures.push(...contents.features)
|
|
||||||
}
|
|
||||||
|
|
||||||
return allFeatures
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetchImageMetadata(datapath: string, image: string): Promise<boolean> {
|
|
||||||
if (image === undefined) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (!image.match(/https:\/\/i\.imgur\.com\/[a-zA-Z0-9]+(\.jpe?g)|(\.png)/)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const filename = image.replace(/[\/:.\-%]/g, "_") + ".json"
|
|
||||||
const targetPath = datapath + "/" + filename
|
|
||||||
if (fs.existsSync(targetPath)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const attribution = await Imgur.singleton.DownloadAttribution({ url: image })
|
|
||||||
|
|
||||||
if ((attribution.artist ?? "") === "") {
|
|
||||||
// This is an invalid attribution. We save the raw response as well
|
|
||||||
const hash = image.substr("https://i.imgur.com/".length).split(".jpg")[0]
|
|
||||||
|
|
||||||
const apiUrl = "https://api.imgur.com/3/image/" + hash
|
|
||||||
const response = await Utils.downloadJsonCached(apiUrl, 365 * 24 * 60 * 60, {
|
|
||||||
Authorization: "Client-ID " + Constants.ImgurApiKey,
|
|
||||||
})
|
|
||||||
const rawTarget = datapath + "/raw/" + filename
|
|
||||||
console.log("Also storing the raw response to", rawTarget)
|
|
||||||
await fs.writeFileSync(rawTarget, JSON.stringify(response, null, " "))
|
|
||||||
}
|
|
||||||
|
|
||||||
await fs.writeFileSync(targetPath, JSON.stringify(attribution, null, " "))
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
loadImageUrls(datapath: string): { allImages: Set<string>; imageSource: Map<string, string> } {
|
|
||||||
let allImages = new Set<string>()
|
|
||||||
const features = this.loadData(datapath)
|
|
||||||
let imageSource: Map<string, string> = new Map<string, string>()
|
|
||||||
|
|
||||||
for (const feature of features) {
|
|
||||||
allImages.add(feature.properties["image"])
|
|
||||||
imageSource[feature.properties["image"]] = feature.properties.id
|
|
||||||
allImages.add(feature.properties["image:streetsign"])
|
|
||||||
imageSource[feature.properties["image:streetsign"]] =
|
|
||||||
feature.properties.id + " (streetsign)"
|
|
||||||
|
|
||||||
for (let i = 0; i < GenerateImageAnalysis.maxImageIndex; i++) {
|
|
||||||
allImages.add(feature.properties["image:" + i])
|
|
||||||
imageSource[
|
|
||||||
feature.properties["image:" + i]
|
|
||||||
] = `${feature.properties.id} (image:${i})`
|
|
||||||
|
|
||||||
allImages.add(feature.properties["image" + i])
|
|
||||||
imageSource[
|
|
||||||
feature.properties["image" + i]
|
|
||||||
] = `${feature.properties.id} (image${i})`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
allImages.delete(undefined)
|
|
||||||
allImages.delete(null)
|
|
||||||
imageSource.delete(undefined)
|
|
||||||
imageSource.delete(null)
|
|
||||||
return { allImages, imageSource }
|
|
||||||
}
|
|
||||||
|
|
||||||
async downloadMetadata(datapath: string): Promise<void> {
|
|
||||||
const { allImages, imageSource } = this.loadImageUrls(datapath)
|
|
||||||
console.log("Detected", allImages.size, "images")
|
|
||||||
let i = 0
|
|
||||||
let d = 0
|
|
||||||
let s = 0
|
|
||||||
let f = 0
|
|
||||||
let start = Date.now()
|
|
||||||
for (const image of Array.from(allImages)) {
|
|
||||||
i++
|
|
||||||
try {
|
|
||||||
const downloaded = await this.fetchImageMetadata(datapath, image)
|
|
||||||
const runningSecs = (Date.now() - start) / 1000
|
|
||||||
const left = allImages.size - i
|
|
||||||
|
|
||||||
const estimatedActualSeconds = Math.floor((left * runningSecs) / (f + d))
|
|
||||||
const estimatedActualMinutes = Math.floor(estimatedActualSeconds / 60)
|
|
||||||
|
|
||||||
const msg = `${i}/${
|
|
||||||
allImages.size
|
|
||||||
} downloaded: ${d},skipped: ${s}, failed: ${f}, running: ${Math.floor(
|
|
||||||
runningSecs
|
|
||||||
)}sec, ETA: ${estimatedActualMinutes}:${estimatedActualSeconds % 60}`
|
|
||||||
if (d + (f % 1000) === 1 || downloaded) {
|
|
||||||
ScriptUtils.erasableLog(msg)
|
|
||||||
}
|
|
||||||
if (downloaded) {
|
|
||||||
d++
|
|
||||||
} else {
|
|
||||||
s++
|
|
||||||
}
|
|
||||||
if (d + f == 75000) {
|
|
||||||
console.log("Used 75000 API calls, leaving 5000 for the rest of the day...")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// console.log(e)
|
|
||||||
console.log(
|
|
||||||
"Offending image hash is",
|
|
||||||
image,
|
|
||||||
"from https://openstreetmap.org/" + imageSource[image]
|
|
||||||
)
|
|
||||||
f++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async downloadImage(url: string, imagePath: string): Promise<boolean> {
|
|
||||||
const filenameLong = url.replace(/[\/:.\-%]/g, "_") + ".jpg"
|
|
||||||
const targetPathLong = imagePath + "/" + filenameLong
|
|
||||||
|
|
||||||
const filename = url.substring("https://i.imgur.com/".length)
|
|
||||||
const targetPath = imagePath + "/" + filename
|
|
||||||
if (fs.existsSync(targetPathLong)) {
|
|
||||||
if (fs.existsSync(targetPath)) {
|
|
||||||
fs.unlinkSync(targetPathLong)
|
|
||||||
console.log("Unlinking duplicate")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
console.log("Renaming...")
|
|
||||||
fs.renameSync(targetPathLong, targetPath)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (fs.existsSync(targetPath)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
await ScriptUtils.DownloadFileTo(url, targetPath)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
async downloadAllImages(datapath: string, imagePath: string): Promise<void> {
|
|
||||||
const { allImages } = this.loadImageUrls(datapath)
|
|
||||||
let skipped = 0
|
|
||||||
let failed = 0
|
|
||||||
let downloaded = 0
|
|
||||||
let invalid = 0
|
|
||||||
const startTime = Date.now()
|
|
||||||
const urls = Array.from(allImages).filter((url) => url.startsWith("https://i.imgur.com"))
|
|
||||||
for (const url of urls) {
|
|
||||||
const runningTime = (Date.now() - startTime) / 1000
|
|
||||||
const handled = skipped + downloaded + failed
|
|
||||||
const itemsLeft = allImages.size - handled
|
|
||||||
const speed = handled / runningTime
|
|
||||||
const timeLeft = Math.round(itemsLeft * speed)
|
|
||||||
try {
|
|
||||||
const urls = url.split(/[;,]/)
|
|
||||||
const downloadedStatus = await Promise.all(
|
|
||||||
urls.map((url) => this.downloadImage(url.trim(), imagePath))
|
|
||||||
)
|
|
||||||
|
|
||||||
for (const b of downloadedStatus) {
|
|
||||||
if (b) {
|
|
||||||
downloaded += 1
|
|
||||||
} else {
|
|
||||||
skipped += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (downloadedStatus.some((i) => i) || skipped % 10000 === 0) {
|
|
||||||
console.log(
|
|
||||||
"Handled",
|
|
||||||
url,
|
|
||||||
JSON.stringify({
|
|
||||||
skipped,
|
|
||||||
failed,
|
|
||||||
downloaded,
|
|
||||||
invalid,
|
|
||||||
total: allImages.size,
|
|
||||||
eta: timeLeft + "s",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e)
|
|
||||||
failed++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
analyze(datapath: string) {
|
|
||||||
const files = ScriptUtils.readDirRecSync(datapath)
|
|
||||||
const byAuthor = new Map<string, string[]>()
|
|
||||||
const byLicense = new Map<string, string[]>()
|
|
||||||
const licenseByAuthor = new Map<string, Set<string>>()
|
|
||||||
for (const file of files) {
|
|
||||||
if (!file.endsWith(".json")) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const attr = <LicenseInfo>JSON.parse(fs.readFileSync(file, { encoding: "utf8" }))
|
|
||||||
const license = attr.licenseShortName
|
|
||||||
|
|
||||||
if (license === undefined || attr.artist === undefined) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (byAuthor.get(attr.artist) === undefined) {
|
|
||||||
byAuthor.set(attr.artist, [])
|
|
||||||
}
|
|
||||||
byAuthor.get(attr.artist).push(file)
|
|
||||||
|
|
||||||
if (byLicense.get(license) === undefined) {
|
|
||||||
byLicense.set(license, [])
|
|
||||||
}
|
|
||||||
byLicense.get(license).push(file)
|
|
||||||
|
|
||||||
if (licenseByAuthor.get(license) === undefined) {
|
|
||||||
licenseByAuthor.set(license, new Set<string>())
|
|
||||||
}
|
|
||||||
licenseByAuthor.get(license).add(attr.artist)
|
|
||||||
}
|
|
||||||
byAuthor.delete(undefined)
|
|
||||||
byLicense.delete(undefined)
|
|
||||||
licenseByAuthor.delete(undefined)
|
|
||||||
|
|
||||||
const byLicenseCount = Utils.MapToObj(byLicense, (a) => a.length)
|
|
||||||
const byAuthorCount = Utils.MapToObj(byAuthor, (a) => a.length)
|
|
||||||
const licenseByAuthorCount = Utils.MapToObj(licenseByAuthor, (a) => a.size)
|
|
||||||
|
|
||||||
const countsPerAuthor: number[] = Array.from(Object.keys(byAuthorCount)).map(
|
|
||||||
(k) => byAuthorCount[k]
|
|
||||||
)
|
|
||||||
console.log(countsPerAuthor)
|
|
||||||
countsPerAuthor.sort()
|
|
||||||
const median = countsPerAuthor[Math.floor(countsPerAuthor.length / 2)]
|
|
||||||
const json: {
|
|
||||||
leaderboard: { rank: number; account: string; name: string; nrOfImages: number }[]
|
|
||||||
} = {
|
|
||||||
leaderboard: [],
|
|
||||||
}
|
|
||||||
for (let i = 0; i < 100; i++) {
|
|
||||||
let maxAuthor: string = undefined
|
|
||||||
let maxCount = 0
|
|
||||||
for (const author in byAuthorCount) {
|
|
||||||
const count = byAuthorCount[author]
|
|
||||||
if (maxAuthor === undefined || count > maxCount) {
|
|
||||||
maxAuthor = author
|
|
||||||
maxCount = count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
json.leaderboard.push({
|
|
||||||
rank: i + 1,
|
|
||||||
name: maxAuthor,
|
|
||||||
account: "https://openstreetmap.org/user/" + maxAuthor.replace(/ /g, "%20"),
|
|
||||||
nrOfImages: maxCount,
|
|
||||||
})
|
|
||||||
console.log(
|
|
||||||
"|",
|
|
||||||
i + 1,
|
|
||||||
"|",
|
|
||||||
`[${maxAuthor}](https://openstreetmap.org/user/${maxAuthor.replace(/ /g, "%20")})`,
|
|
||||||
"|",
|
|
||||||
maxCount,
|
|
||||||
"|"
|
|
||||||
)
|
|
||||||
delete byAuthorCount[maxAuthor]
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalAuthors = byAuthor.size
|
|
||||||
let totalLicensedImages = 0
|
|
||||||
json["totalAuthors"] = totalAuthors
|
|
||||||
for (const license in byLicenseCount) {
|
|
||||||
totalLicensedImages += byLicenseCount[license]
|
|
||||||
}
|
|
||||||
json["byLicense"] = {}
|
|
||||||
for (const license in byLicenseCount) {
|
|
||||||
const total = byLicenseCount[license]
|
|
||||||
const authors = licenseByAuthorCount[license]
|
|
||||||
console.log(
|
|
||||||
`License ${license}: ${total} total pictures (${
|
|
||||||
Math.floor((1000 * total) / totalLicensedImages) / 10
|
|
||||||
}%), ${authors} authors (${
|
|
||||||
Math.floor((1000 * authors) / totalAuthors) / 10
|
|
||||||
}%), ${Math.floor(total / authors)} images/author`
|
|
||||||
)
|
|
||||||
json["byLicense"] = {
|
|
||||||
license,
|
|
||||||
total,
|
|
||||||
authors,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const nonDefaultAuthors = [
|
|
||||||
...Array.from(licenseByAuthor.get("CC-BY 4.0").values()),
|
|
||||||
...Array.from(licenseByAuthor.get("CC-BY-SA 4.0").values()),
|
|
||||||
]
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
"Total number of correctly licenses pictures: ",
|
|
||||||
totalLicensedImages,
|
|
||||||
"(out of ",
|
|
||||||
files.length,
|
|
||||||
" images)"
|
|
||||||
)
|
|
||||||
console.log("Total number of authors:", byAuthor.size)
|
|
||||||
console.log(
|
|
||||||
"Total number of authors which used a valid, non CC0 license at one point in time",
|
|
||||||
nonDefaultAuthors.length
|
|
||||||
)
|
|
||||||
console.log("Median contributions per author:", median)
|
|
||||||
json["median"] = median
|
|
||||||
json["date"] = new Date().toISOString()
|
|
||||||
writeFileSync(
|
|
||||||
"../../git/MapComplete-data/picture-leaderboard.json",
|
|
||||||
JSON.stringify(json),
|
|
||||||
"utf8"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async main(args: string[]): Promise<void> {
|
|
||||||
console.log("Usage: [--cached] to use the cached osm data")
|
|
||||||
console.log("Args are", args)
|
|
||||||
const cached = args.indexOf("--cached") < 0
|
|
||||||
args = args.filter((a) => a !== "--cached")
|
|
||||||
const datapath = args[1] ?? "../../git/MapComplete-data/ImageLicenseInfo"
|
|
||||||
const imageBackupPath = args[0]
|
|
||||||
if (imageBackupPath === "" || imageBackupPath === undefined) {
|
|
||||||
throw "No imageBackup path specified"
|
|
||||||
}
|
|
||||||
await this.downloadData(datapath, cached)
|
|
||||||
|
|
||||||
// await this.downloadViews(datapath)
|
|
||||||
await this.downloadMetadata(datapath)
|
|
||||||
await this.downloadAllImages(datapath, imageBackupPath)
|
|
||||||
this.analyze(datapath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
new GenerateImageAnalysis().run()
|
|
|
@ -1,71 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { Utils } from "../Utils"
|
|
||||||
import { Store, UIEventSource } from "../Logic/UIEventSource"
|
|
||||||
import Loading from "./Base/Loading.svelte"
|
|
||||||
import { OsmConnection } from "../Logic/Osm/OsmConnection"
|
|
||||||
|
|
||||||
const osmConnection = new OsmConnection({
|
|
||||||
attemptLogin: true,
|
|
||||||
})
|
|
||||||
let loggedInContributor: Store<string> = osmConnection.userDetails.map((ud) => ud.name)
|
|
||||||
export let source =
|
|
||||||
"https://raw.githubusercontent.com/pietervdvn/MapComplete-data/main/picture-leaderboard.json"
|
|
||||||
let data: Store<
|
|
||||||
| undefined
|
|
||||||
| {
|
|
||||||
leaderboard: {
|
|
||||||
rank: number
|
|
||||||
name: string
|
|
||||||
account: string
|
|
||||||
nrOfImages: number
|
|
||||||
}[]
|
|
||||||
median: number
|
|
||||||
totalAuthors: number
|
|
||||||
byLicense: {
|
|
||||||
license: string
|
|
||||||
total: number
|
|
||||||
authors: string[]
|
|
||||||
}
|
|
||||||
date: string
|
|
||||||
}
|
|
||||||
> = UIEventSource.FromPromise(Utils.downloadJsonCached(source))
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<main>
|
|
||||||
<h1>Contributed images with MapComplete: leaderboard</h1>
|
|
||||||
|
|
||||||
{#if $data}
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<th>Rank</th>
|
|
||||||
<th>Contributor</th>
|
|
||||||
<th>Number of images contributed</th>
|
|
||||||
</tr>
|
|
||||||
{#each $data.leaderboard as contributor}
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
{contributor.rank}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{#if $loggedInContributor === contributor.name}
|
|
||||||
<a class="thanks" href={contributor.account}>{contributor.name}</a>
|
|
||||||
{:else}
|
|
||||||
<a href={contributor.account}>{contributor.name}</a>
|
|
||||||
{/if}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<b>{contributor.nrOfImages}</b>
|
|
||||||
total images
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</table>
|
|
||||||
Statistics generated on {$data.date}
|
|
||||||
{:else}
|
|
||||||
<Loading />
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div>
|
|
||||||
Logged in as {$loggedInContributor}
|
|
||||||
</div>
|
|
||||||
</main>
|
|
|
@ -1,5 +0,0 @@
|
||||||
import Leaderboard from "./UI/Leaderboard.svelte"
|
|
||||||
|
|
||||||
new Leaderboard({
|
|
||||||
target: document.getElementById("main"),
|
|
||||||
})
|
|
Loading…
Add table
Add a link
Reference in a new issue