forked from MapComplete/MapComplete
		
	Copyright panel: port to svelte, generate licenses detects 'mostly white' icons now, fix #2041
This commit is contained in:
		
							parent
							
								
									f2d2240896
								
							
						
					
					
						commit
						2aa77b7b47
					
				
					 9 changed files with 331 additions and 245 deletions
				
			
		|  | @ -2730,6 +2730,11 @@ video { | ||||||
|   background-color: rgb(248 113 113 / var(--tw-bg-opacity)); |   background-color: rgb(248 113 113 / var(--tw-bg-opacity)); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .bg-slate-400 { | ||||||
|  |   --tw-bg-opacity: 1; | ||||||
|  |   background-color: rgb(148 163 184 / var(--tw-bg-opacity)); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .bg-black { | .bg-black { | ||||||
|   --tw-bg-opacity: 1; |   --tw-bg-opacity: 1; | ||||||
|   background-color: rgb(0 0 0 / var(--tw-bg-opacity)); |   background-color: rgb(0 0 0 / var(--tw-bg-opacity)); | ||||||
|  |  | ||||||
|  | @ -123,7 +123,7 @@ export default class ScriptUtils { | ||||||
|         return ScriptUtils.DownloadJSON(url) |         return ScriptUtils.DownloadJSON(url) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static async ReadSvg(path: string): Promise<any> { |     public static async ReadSvg(path: string): Promise<SVGElement> { | ||||||
|         if (!existsSync(path)) { |         if (!existsSync(path)) { | ||||||
|             throw "File not found: " + path |             throw "File not found: " + path | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -3,7 +3,9 @@ import SmallLicense from "../src/Models/smallLicense" | ||||||
| import ScriptUtils from "./ScriptUtils" | import ScriptUtils from "./ScriptUtils" | ||||||
| import Script from "./Script" | import Script from "./Script" | ||||||
| import { Utils } from "../src/Utils" | import { Utils } from "../src/Utils" | ||||||
|  | 
 | ||||||
| const prompt = require("prompt-sync")() | const prompt = require("prompt-sync")() | ||||||
|  | 
 | ||||||
| export class GenerateLicenseInfo extends Script { | export class GenerateLicenseInfo extends Script { | ||||||
|     private static readonly needsLicenseRef = new Set( |     private static readonly needsLicenseRef = new Set( | ||||||
|         ScriptUtils.readDirRecSync("./LICENSES") |         ScriptUtils.readDirRecSync("./LICENSES") | ||||||
|  | @ -23,7 +25,7 @@ export class GenerateLicenseInfo extends Script { | ||||||
|             authors: ["Pieter Vander Vennet"], |             authors: ["Pieter Vander Vennet"], | ||||||
|             path: undefined, |             path: undefined, | ||||||
|             license: "CC0", |             license: "CC0", | ||||||
|             sources: [], |             sources: [] | ||||||
|         }) |         }) | ||||||
|         knownLicenses.set("streetcomplete", { |         knownLicenses.set("streetcomplete", { | ||||||
|             authors: ["Tobias Zwick (westnordost)"], |             authors: ["Tobias Zwick (westnordost)"], | ||||||
|  | @ -31,8 +33,8 @@ export class GenerateLicenseInfo extends Script { | ||||||
|             license: "CC0", |             license: "CC0", | ||||||
|             sources: [ |             sources: [ | ||||||
|                 "https://github.com/streetcomplete/StreetComplete/tree/master/res/graphics", |                 "https://github.com/streetcomplete/StreetComplete/tree/master/res/graphics", | ||||||
|                 "https://f-droid.org/packages/de.westnordost.streetcomplete/", |                 "https://f-droid.org/packages/de.westnordost.streetcomplete/" | ||||||
|             ], |             ] | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         knownLicenses.set("temaki", { |         knownLicenses.set("temaki", { | ||||||
|  | @ -41,34 +43,34 @@ export class GenerateLicenseInfo extends Script { | ||||||
|             license: "CC0", |             license: "CC0", | ||||||
|             sources: [ |             sources: [ | ||||||
|                 "https://github.com/ideditor/temaki", |                 "https://github.com/ideditor/temaki", | ||||||
|                 "https://ideditor.github.io/temaki/docs/", |                 "https://ideditor.github.io/temaki/docs/" | ||||||
|             ], |             ] | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         knownLicenses.set("maki", { |         knownLicenses.set("maki", { | ||||||
|             authors: ["Maki"], |             authors: ["Maki"], | ||||||
|             path: undefined, |             path: undefined, | ||||||
|             license: "CC0", |             license: "CC0", | ||||||
|             sources: ["https://labs.mapbox.com/maki-icons/"], |             sources: ["https://labs.mapbox.com/maki-icons/"] | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         knownLicenses.set("t", { |         knownLicenses.set("t", { | ||||||
|             authors: [], |             authors: [], | ||||||
|             path: undefined, |             path: undefined, | ||||||
|             license: "CC0; trivial", |             license: "CC0; trivial", | ||||||
|             sources: [], |             sources: [] | ||||||
|         }) |         }) | ||||||
|         knownLicenses.set("na", { |         knownLicenses.set("na", { | ||||||
|             authors: [], |             authors: [], | ||||||
|             path: undefined, |             path: undefined, | ||||||
|             license: "CC0", |             license: "CC0", | ||||||
|             sources: [], |             sources: [] | ||||||
|         }) |         }) | ||||||
|         knownLicenses.set("carto", { |         knownLicenses.set("carto", { | ||||||
|             authors: ["OSM-Carto"], |             authors: ["OSM-Carto"], | ||||||
|             path: undefined, |             path: undefined, | ||||||
|             license: "CC0", |             license: "CC0", | ||||||
|             sources: [""], |             sources: [""] | ||||||
|         }) |         }) | ||||||
|         knownLicenses.set("tv", { |         knownLicenses.set("tv", { | ||||||
|             authors: ["Toerisme Vlaanderen"], |             authors: ["Toerisme Vlaanderen"], | ||||||
|  | @ -76,20 +78,20 @@ export class GenerateLicenseInfo extends Script { | ||||||
|             license: "CC0", |             license: "CC0", | ||||||
|             sources: [ |             sources: [ | ||||||
|                 "https://toerismevlaanderen.be/pinjepunt", |                 "https://toerismevlaanderen.be/pinjepunt", | ||||||
|                 "https://mapcomplete.org/toerisme_vlaanderenn", |                 "https://mapcomplete.org/toerisme_vlaanderenn" | ||||||
|             ], |             ] | ||||||
|         }) |         }) | ||||||
|         knownLicenses.set("tvf", { |         knownLicenses.set("tvf", { | ||||||
|             authors: ["Jo De Baerdemaeker "], |             authors: ["Jo De Baerdemaeker "], | ||||||
|             path: undefined, |             path: undefined, | ||||||
|             license: "All rights reserved", |             license: "All rights reserved", | ||||||
|             sources: ["https://www.studiotype.be/fonts/flandersart"], |             sources: ["https://www.studiotype.be/fonts/flandersart"] | ||||||
|         }) |         }) | ||||||
|         knownLicenses.set("twemoji", { |         knownLicenses.set("twemoji", { | ||||||
|             authors: ["Twemoji"], |             authors: ["Twemoji"], | ||||||
|             path: undefined, |             path: undefined, | ||||||
|             license: "CC-BY 4.0", |             license: "CC-BY 4.0", | ||||||
|             sources: ["https://github.com/twitter/twemoji"], |             sources: ["https://github.com/twitter/twemoji"] | ||||||
|         }) |         }) | ||||||
|         return knownLicenses |         return knownLicenses | ||||||
|     } |     } | ||||||
|  | @ -135,6 +137,50 @@ export class GenerateLicenseInfo extends Script { | ||||||
|         return licenses |         return licenses | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     async mostlyWhite(allIcons: string[]) { | ||||||
|  |         const whitePaths = new Set<string>() | ||||||
|  |         for (const icon of allIcons) { | ||||||
|  |             if (!icon.endsWith(".svg")) { | ||||||
|  |                 continue | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             const svg = await ScriptUtils.ReadSvg(icon) | ||||||
|  | 
 | ||||||
|  |             const colours = new Set<string>() | ||||||
|  |             Utils.WalkObject(svg, leaf => { | ||||||
|  |                 const style = leaf["style"].split(";") | ||||||
|  |                 for (const styleElement of style) { | ||||||
|  |                     const [key, value] = styleElement.split(":").map(x => x.trim()) | ||||||
|  |                     if (value === "none") { | ||||||
|  |                         continue | ||||||
|  |                     } | ||||||
|  |                     if (key === "fill" || key === "stroke") { | ||||||
|  |                         colours.add(value) | ||||||
|  |                     } | ||||||
|  |                     return colours | ||||||
|  |                 } | ||||||
|  |             }, leaf => typeof leaf["style"] === "string" ) | ||||||
|  |             if(colours.size === 0){ | ||||||
|  |                 continue | ||||||
|  |             } | ||||||
|  |             const whiteColours = Array.from(colours).map(c => { | ||||||
|  |                 const rgb = Utils.color(c) | ||||||
|  |                 if(!rgb){ | ||||||
|  |                     console.log("Could not parse ", c) | ||||||
|  |                     return false | ||||||
|  |                 } | ||||||
|  |                 const {r,g,b} = rgb | ||||||
|  |                 return (r > 245 && g > 245 && b > 245) | ||||||
|  |             }) | ||||||
|  |             const hasDark = whiteColours.some(isWhite => !isWhite) | ||||||
|  |             if(!hasDark){ | ||||||
|  |                 whitePaths.add(icon) | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |         } | ||||||
|  |         return whitePaths | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     missingLicenseInfos(licenseInfos: SmallLicense[], allIcons: string[]) { |     missingLicenseInfos(licenseInfos: SmallLicense[], allIcons: string[]) { | ||||||
|         const missing = [] |         const missing = [] | ||||||
| 
 | 
 | ||||||
|  | @ -182,7 +228,7 @@ export class GenerateLicenseInfo extends Script { | ||||||
|             authors: author.split(";"), |             authors: author.split(";"), | ||||||
|             path: path, |             path: path, | ||||||
|             license: prompt("What is the license for artwork " + path + "?  > "), |             license: prompt("What is the license for artwork " + path + "?  > "), | ||||||
|             sources: prompt("Where was this artwork found?  > ").split(";"), |             sources: prompt("Where was this artwork found?  > ").split(";") | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -215,7 +261,7 @@ export class GenerateLicenseInfo extends Script { | ||||||
|             "ISC-LICENSE": "ISC", |             "ISC-LICENSE": "ISC", | ||||||
|             "LOGO-BY-THE-GOVERNMENT": "LOGO", |             "LOGO-BY-THE-GOVERNMENT": "LOGO", | ||||||
|             PD: "PUBLIC-DOMAIN", |             PD: "PUBLIC-DOMAIN", | ||||||
|             "LOGO-(ALL-RIGHTS-RESERVED)": "LOGO", |             "LOGO-(ALL-RIGHTS-RESERVED)": "LOGO" | ||||||
|             /*  ALL-RIGHTS-RESERVED: |             /*  ALL-RIGHTS-RESERVED: | ||||||
|             PD: |             PD: | ||||||
|                 PUBLIC-DOMAIN: |                 PUBLIC-DOMAIN: | ||||||
|  | @ -246,7 +292,7 @@ export class GenerateLicenseInfo extends Script { | ||||||
|                 path: license.path, |                 path: license.path, | ||||||
|                 license: license.license, |                 license: license.license, | ||||||
|                 authors: license.authors, |                 authors: license.authors, | ||||||
|                 sources: license.sources, |                 sources: license.sources | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             cloned.license = Utils.Dedup( |             cloned.license = Utils.Dedup( | ||||||
|  | @ -289,7 +335,7 @@ export class GenerateLicenseInfo extends Script { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     queryMissingLicenses(missingLicenses: string[]) { |     queryMissingLicenses(missingLicenses: string[]) { | ||||||
|         process.on("SIGINT", function () { |         process.on("SIGINT", function() { | ||||||
|             console.log("Aborting... Bye!") |             console.log("Aborting... Bye!") | ||||||
|             process.exit() |             process.exit() | ||||||
|         }) |         }) | ||||||
|  | @ -311,7 +357,7 @@ export class GenerateLicenseInfo extends Script { | ||||||
|      * Creates the humongous license_info in the generated assets, containing all licenses with a path relative to the root |      * Creates the humongous license_info in the generated assets, containing all licenses with a path relative to the root | ||||||
|      * @param licensePaths |      * @param licensePaths | ||||||
|      */ |      */ | ||||||
|     createFullLicenseOverview(licensePaths: string[]) { |     createFullLicenseOverview(licensePaths: string[], mostlyWhite: string[]) { | ||||||
|         const allLicenses: SmallLicense[] = [] |         const allLicenses: SmallLicense[] = [] | ||||||
|         for (const licensePath of licensePaths) { |         for (const licensePath of licensePaths) { | ||||||
|             if (!existsSync(licensePath)) { |             if (!existsSync(licensePath)) { | ||||||
|  | @ -327,6 +373,9 @@ export class GenerateLicenseInfo extends Script { | ||||||
|                     licensePath.length - "license_info.json".length |                     licensePath.length - "license_info.json".length | ||||||
|                 ) |                 ) | ||||||
|                 license.path = dir + license.path |                 license.path = dir + license.path | ||||||
|  |                 if(mostlyWhite.some(l => license.path === l)){ | ||||||
|  |                     license["mostly_white"] = true | ||||||
|  |                 } | ||||||
|                 allLicenses.push(license) |                 allLicenses.push(license) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  | @ -354,6 +403,7 @@ export class GenerateLicenseInfo extends Script { | ||||||
|             (pth) => pth.match(/(.svg|.png|.jpg|.ttf|.otf|.woff|.jpeg)$/i) != null |             (pth) => pth.match(/(.svg|.png|.jpg|.ttf|.otf|.woff|.jpeg)$/i) != null | ||||||
|         ) |         ) | ||||||
|         const missingLicenses = this.missingLicenseInfos(licenseInfos, artwork) |         const missingLicenses = this.missingLicenseInfos(licenseInfos, artwork) | ||||||
|  |         const mostlyWhite: Set<string> = await this.mostlyWhite(artwork) | ||||||
|         if (args.indexOf("--prompt") >= 0 || args.indexOf("--query") >= 0) { |         if (args.indexOf("--prompt") >= 0 || args.indexOf("--query") >= 0) { | ||||||
|             this.queryMissingLicenses(missingLicenses) |             this.queryMissingLicenses(missingLicenses) | ||||||
|             return this.main([]) |             return this.main([]) | ||||||
|  | @ -371,7 +421,7 @@ export class GenerateLicenseInfo extends Script { | ||||||
|             if (licenseInfo.sources.length + licenseInfo.authors.length == 0 && !isTrivial) { |             if (licenseInfo.sources.length + licenseInfo.authors.length == 0 && !isTrivial) { | ||||||
|                 invalidLicenses.push( |                 invalidLicenses.push( | ||||||
|                     "Invalid license: No sources nor authors given in the license for " + |                     "Invalid license: No sources nor authors given in the license for " + | ||||||
|                         JSON.stringify(licenseInfo) |                     JSON.stringify(licenseInfo) | ||||||
|                 ) |                 ) | ||||||
|                 continue |                 continue | ||||||
|             } |             } | ||||||
|  | @ -394,10 +444,10 @@ export class GenerateLicenseInfo extends Script { | ||||||
|             const spdxContent = [ |             const spdxContent = [ | ||||||
|                 "SPDX-FileCopyrightText: " + licenseInfo.authors.join("; "), |                 "SPDX-FileCopyrightText: " + licenseInfo.authors.join("; "), | ||||||
|                 "SPDX-License-Identifier: " + |                 "SPDX-License-Identifier: " + | ||||||
|                     licenseInfo.license |                 licenseInfo.license | ||||||
|                         .split(" AND ") |                     .split(" AND ") | ||||||
|                         .map((s) => this.addLicenseRef(s)) |                     .map((s) => this.addLicenseRef(s)) | ||||||
|                         .join(" AND "), |                     .join(" AND ") | ||||||
|             ] |             ] | ||||||
|             writeFileSync(spdxPath, spdxContent.join("\n")) |             writeFileSync(spdxPath, spdxContent.join("\n")) | ||||||
|         } |         } | ||||||
|  | @ -412,7 +462,7 @@ export class GenerateLicenseInfo extends Script { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         this.cleanLicenseInfo(licensePaths, licenseInfos) |         this.cleanLicenseInfo(licensePaths, licenseInfos) | ||||||
|         this.createFullLicenseOverview(licensePaths) |         this.createFullLicenseOverview(licensePaths, Array.from(mostlyWhite)) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -422,7 +472,6 @@ export class GenerateLicenseInfo extends Script { | ||||||
|      */ |      */ | ||||||
|     private addLicenseRef(s: string): string { |     private addLicenseRef(s: string): string { | ||||||
|         if (GenerateLicenseInfo.needsLicenseRef.has(s)) { |         if (GenerateLicenseInfo.needsLicenseRef.has(s)) { | ||||||
|             console.log("Mapping ", s, Array.from(GenerateLicenseInfo.needsLicenseRef)) |  | ||||||
|             return "LicenseRef-" + s |             return "LicenseRef-" + s | ||||||
|         } |         } | ||||||
|         return s |         return s | ||||||
|  |  | ||||||
|  | @ -3,4 +3,5 @@ export default interface SmallLicense { | ||||||
|     authors: string[] |     authors: string[] | ||||||
|     license: string |     license: string | ||||||
|     sources: string[] |     sources: string[] | ||||||
|  |     mostly_white?: boolean | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										188
									
								
								src/UI/BigComponents/CopyrightPanel.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										188
									
								
								src/UI/BigComponents/CopyrightPanel.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,188 @@ | ||||||
|  | <script lang="ts"> | ||||||
|  |   import type { SpecialVisualizationState } from "../SpecialVisualization" | ||||||
|  |   import Translations from "../i18n/Translations" | ||||||
|  |   import contributors from "../../assets/contributors.json" | ||||||
|  |   import translators from "../../assets/translators.json" | ||||||
|  |   import { Translation, TypedTranslation } from "../i18n/Translation" | ||||||
|  |   import AccordionSingle from "../Flowbite/AccordionSingle.svelte" | ||||||
|  |   import Tr from "../Base/Tr.svelte" | ||||||
|  |   import IconCopyrightPanel from "./IconCopyrightPanel.svelte" | ||||||
|  |   import licenses from "../../assets/generated/license_info.json" | ||||||
|  |   import type SmallLicense from "../../Models/smallLicense" | ||||||
|  |   import Constants from "../../Models/Constants" | ||||||
|  |   import ContributorCount from "../../Logic/ContributorCount" | ||||||
|  |   import BaseUIElement from "../BaseUIElement" | ||||||
|  |   import Github from "../../assets/svg/Github.svelte" | ||||||
|  |   import { DatabaseIcon, TranslateIcon } from "@rgossiaux/svelte-heroicons/solid" | ||||||
|  |   import Osm_logo from "../../assets/svg/Osm_logo.svelte" | ||||||
|  |   import Generic_map from "../../assets/svg/Generic_map.svelte" | ||||||
|  |   import { PencilIcon, UserGroupIcon, UsersIcon } from "@babeard/svelte-heroicons/solid" | ||||||
|  |   import Loading from "../Base/Loading.svelte" | ||||||
|  |   import Marker from "../Map/Marker.svelte" | ||||||
|  | 
 | ||||||
|  |   export let state: SpecialVisualizationState | ||||||
|  | 
 | ||||||
|  |   const t = Translations.t.general.attribution | ||||||
|  |   const layoutToUse = state.layout | ||||||
|  | 
 | ||||||
|  |   const iconAttributions: string[] = layoutToUse.getUsedImages() | ||||||
|  | 
 | ||||||
|  |   let maintainer: Translation = undefined | ||||||
|  |   if (layoutToUse.credits !== undefined && layoutToUse.credits !== "") { | ||||||
|  |     maintainer = t.themeBy.Subs({ author: layoutToUse.credits }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   const bgMapAttribution = state.mapProperties.rasterLayer.mapD((layer) => { | ||||||
|  |     const props = layer.properties | ||||||
|  |     const attrUrl = props.attribution?.url | ||||||
|  |     const attrText = props.attribution?.text | ||||||
|  | 
 | ||||||
|  |     let bgAttr: BaseUIElement | string = undefined | ||||||
|  |     if (attrText && attrUrl) { | ||||||
|  |       bgAttr = | ||||||
|  |         "<a href='" + | ||||||
|  |         attrUrl + | ||||||
|  |         "' target='_blank' rel='noopener'>" + | ||||||
|  |         attrText + | ||||||
|  |         "</a>" | ||||||
|  |     } else if (attrUrl) { | ||||||
|  |       bgAttr = attrUrl | ||||||
|  |     } else { | ||||||
|  |       bgAttr = attrText | ||||||
|  |     } | ||||||
|  |     if (bgAttr) { | ||||||
|  |       return Translations.t.general.attribution.attributionBackgroundLayerWithCopyright.Subs( | ||||||
|  |         { | ||||||
|  |           name: props.name, | ||||||
|  |           copyright: bgAttr | ||||||
|  |         } | ||||||
|  |       ) | ||||||
|  |     } | ||||||
|  |     return Translations.t.general.attribution.attributionBackgroundLayer.Subs( | ||||||
|  |       props | ||||||
|  |     ) | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|  |   const allLicenses = {} | ||||||
|  |   for (const key in licenses) { | ||||||
|  |     const license: SmallLicense = licenses[key] | ||||||
|  |     allLicenses[license.path] = license | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |   function calculateDataContributions(contributions: Map<string, number>): Translation { | ||||||
|  |     if (contributions === undefined) { | ||||||
|  |       return undefined | ||||||
|  |     } | ||||||
|  |     const sorted = Array.from(contributions, ([name, value]) => ({ | ||||||
|  |       name, | ||||||
|  |       value | ||||||
|  |     })).filter((x) => x.name !== undefined && x.name !== "undefined") | ||||||
|  |     if (sorted.length === 0) { | ||||||
|  |       return undefined | ||||||
|  |     } | ||||||
|  |     sorted.sort((a, b) => b.value - a.value) | ||||||
|  |     let hiddenCount = 0 | ||||||
|  |     if (sorted.length > 10) { | ||||||
|  |       hiddenCount = sorted.length - 10 | ||||||
|  |       sorted.splice(10, sorted.length - 10) | ||||||
|  |     } | ||||||
|  |     const links = sorted.map( | ||||||
|  |       (kv) => | ||||||
|  |         `<a href="https://openstreetmap.org/user/${kv.name}" target="_blank">${kv.name}</a>` | ||||||
|  |     ) | ||||||
|  |     const contribs = links.join(", ") | ||||||
|  | 
 | ||||||
|  |     if (hiddenCount <= 0) { | ||||||
|  |       return t.mapContributionsBy.Subs({ | ||||||
|  |         contributors: contribs | ||||||
|  |       }) | ||||||
|  |     } else { | ||||||
|  |       return t.mapContributionsByAndHidden.Subs({ | ||||||
|  |         contributors: contribs, | ||||||
|  |         hiddenCount: hiddenCount | ||||||
|  |       }) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   const datacontributions = new ContributorCount(state).Contributors.map(counts => calculateDataContributions(counts)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |   function codeContributors(contributors, | ||||||
|  |                             translation: TypedTranslation<{ contributors; hiddenCount }>): Translation { | ||||||
|  | 
 | ||||||
|  |     const total = contributors.contributors.length | ||||||
|  |     let filtered = [...contributors.contributors] | ||||||
|  | 
 | ||||||
|  |     filtered.splice(10, total - 10) | ||||||
|  | 
 | ||||||
|  |     let contribsStr = filtered.map((c) => c.contributor).join(", ") | ||||||
|  | 
 | ||||||
|  |     if (contribsStr === "") { | ||||||
|  |       // Hmm, something went wrong loading the contributors list. Lets show nothing | ||||||
|  |       return undefined | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return translation.Subs({ | ||||||
|  |       contributors: contribsStr, | ||||||
|  |       hiddenCount: total - 10 | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <div class="flex flex-col gap-y-4 link-underline"> | ||||||
|  |   <h3> | ||||||
|  |     <Tr t={t.attributionTitle} /> | ||||||
|  |   </h3> | ||||||
|  |   <div class="flex items-center gap-x-2"> | ||||||
|  |     <Osm_logo class="w-8 h-8 shrink-0" /> | ||||||
|  |     <Tr t={t.attributionContent} /> | ||||||
|  |   </div> | ||||||
|  | 
 | ||||||
|  |   {#if $bgMapAttribution !== undefined} | ||||||
|  |     <div class="flex items-center gap-x-2"> | ||||||
|  |       <Generic_map class="w-8 h-8 shrink-0" /> | ||||||
|  |       <Tr t={$bgMapAttribution} /> | ||||||
|  |     </div> | ||||||
|  |   {/if} | ||||||
|  |   <div class="flex items-center gap-x-2"> | ||||||
|  |     <Marker icons={state.layout.icon} size="h-8 w-8 shrink-0" /> | ||||||
|  |     <Tr t={maintainer} /> | ||||||
|  |   </div> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |   <div class="flex items-center gap-x-2"> | ||||||
|  |     <UserGroupIcon class="w-8 h-8 shrink-0" /> | ||||||
|  |     {#if $datacontributions !== undefined} | ||||||
|  |       <Tr t={$datacontributions} /> | ||||||
|  |     {:else} | ||||||
|  |       <Loading /> | ||||||
|  |     {/if} | ||||||
|  |   </div> | ||||||
|  | 
 | ||||||
|  |   <div class="flex items-center gap-x-2"> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     <Github class="w-8 h-8 shrink-0" /> | ||||||
|  |     <Tr t={codeContributors(contributors, t.codeContributionsBy)} /> | ||||||
|  |   </div> | ||||||
|  | 
 | ||||||
|  |   <div class="flex items-center gap-x-2"> | ||||||
|  |     <TranslateIcon class="w-8 h-8 shrink-0" /> | ||||||
|  |     <Tr t={codeContributors(translators, t.translatedBy)} /> | ||||||
|  |   </div> | ||||||
|  | 
 | ||||||
|  |   <AccordionSingle> | ||||||
|  |     <div slot="header"> | ||||||
|  |       <Tr t={t.iconAttribution.title} /> | ||||||
|  |     </div> | ||||||
|  |     {#each iconAttributions as iconAttribution} | ||||||
|  |       <IconCopyrightPanel iconPath={iconAttribution} license={allLicenses[iconAttribution]} /> | ||||||
|  |     {/each} | ||||||
|  |   </AccordionSingle> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |   <div class="self-end"> | ||||||
|  |     MapComplete {Constants.vNumber} | ||||||
|  |   </div> | ||||||
|  | </div> | ||||||
|  | @ -1,213 +0,0 @@ | ||||||
| import Combine from "../Base/Combine" |  | ||||||
| import Translations from "../i18n/Translations" |  | ||||||
| import { Store } from "../../Logic/UIEventSource" |  | ||||||
| import { FixedUiElement } from "../Base/FixedUiElement" |  | ||||||
| import licenses from "../../assets/generated/license_info.json" |  | ||||||
| import SmallLicense from "../../Models/smallLicense" |  | ||||||
| import { Utils } from "../../Utils" |  | ||||||
| import Link from "../Base/Link" |  | ||||||
| import { VariableUiElement } from "../Base/VariableUIElement" |  | ||||||
| import contributors from "../../assets/contributors.json" |  | ||||||
| import translators from "../../assets/translators.json" |  | ||||||
| import BaseUIElement from "../BaseUIElement" |  | ||||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" |  | ||||||
| import Title from "../Base/Title" |  | ||||||
| import { BBox } from "../../Logic/BBox" |  | ||||||
| import { OsmConnection } from "../../Logic/Osm/OsmConnection" |  | ||||||
| import Constants from "../../Models/Constants" |  | ||||||
| import ContributorCount from "../../Logic/ContributorCount" |  | ||||||
| import Img from "../Base/Img" |  | ||||||
| import { TypedTranslation } from "../i18n/Translation" |  | ||||||
| import GeoIndexedStore from "../../Logic/FeatureSource/Actors/GeoIndexedStore" |  | ||||||
| import { RasterLayerPolygon } from "../../Models/RasterLayers" |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * The attribution panel in the theme menu. Shows the licenses of the artwork and of the map data |  | ||||||
|  */ |  | ||||||
| export default class CopyrightPanel extends Combine { |  | ||||||
|     private static LicenseObject = CopyrightPanel.GenerateLicenses() |  | ||||||
| 
 |  | ||||||
|     constructor(state: { |  | ||||||
|         layout: LayoutConfig |  | ||||||
|         mapProperties: { |  | ||||||
|             readonly bounds: Store<BBox> |  | ||||||
|             readonly rasterLayer: Store<RasterLayerPolygon> |  | ||||||
|         } |  | ||||||
|         osmConnection: OsmConnection |  | ||||||
|         dataIsLoading: Store<boolean> |  | ||||||
|         perLayer: ReadonlyMap<string, GeoIndexedStore> |  | ||||||
|     }) { |  | ||||||
|         const t = Translations.t.general.attribution |  | ||||||
|         const layoutToUse = state.layout |  | ||||||
| 
 |  | ||||||
|         const iconAttributions: BaseUIElement[] = layoutToUse |  | ||||||
|             .getUsedImages() |  | ||||||
|             .map(CopyrightPanel.IconAttribution) |  | ||||||
| 
 |  | ||||||
|         let maintainer: BaseUIElement = undefined |  | ||||||
|         if (layoutToUse.credits !== undefined && layoutToUse.credits !== "") { |  | ||||||
|             maintainer = t.themeBy.Subs({ author: layoutToUse.credits }) |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         const contributions = new ContributorCount(state).Contributors |  | ||||||
| 
 |  | ||||||
|         const dataContributors = new VariableUiElement( |  | ||||||
|             contributions.map((contributions) => { |  | ||||||
|                 if (contributions === undefined) { |  | ||||||
|                     return "" |  | ||||||
|                 } |  | ||||||
|                 const sorted = Array.from(contributions, ([name, value]) => ({ |  | ||||||
|                     name, |  | ||||||
|                     value, |  | ||||||
|                 })).filter((x) => x.name !== undefined && x.name !== "undefined") |  | ||||||
|                 if (sorted.length === 0) { |  | ||||||
|                     return "" |  | ||||||
|                 } |  | ||||||
|                 sorted.sort((a, b) => b.value - a.value) |  | ||||||
|                 let hiddenCount = 0 |  | ||||||
|                 if (sorted.length > 10) { |  | ||||||
|                     hiddenCount = sorted.length - 10 |  | ||||||
|                     sorted.splice(10, sorted.length - 10) |  | ||||||
|                 } |  | ||||||
|                 const links = sorted.map( |  | ||||||
|                     (kv) => |  | ||||||
|                         `<a href="https://openstreetmap.org/user/${kv.name}" target="_blank">${kv.name}</a>` |  | ||||||
|                 ) |  | ||||||
|                 const contribs = links.join(", ") |  | ||||||
| 
 |  | ||||||
|                 if (hiddenCount <= 0) { |  | ||||||
|                     return t.mapContributionsBy.Subs({ |  | ||||||
|                         contributors: contribs, |  | ||||||
|                     }) |  | ||||||
|                 } else { |  | ||||||
|                     return t.mapContributionsByAndHidden.Subs({ |  | ||||||
|                         contributors: contribs, |  | ||||||
|                         hiddenCount: hiddenCount, |  | ||||||
|                     }) |  | ||||||
|                 } |  | ||||||
|             }) |  | ||||||
|         ) |  | ||||||
| 
 |  | ||||||
|         super( |  | ||||||
|             [ |  | ||||||
|                 new Title(t.attributionTitle), |  | ||||||
|                 t.attributionContent, |  | ||||||
| 
 |  | ||||||
|                 new VariableUiElement( |  | ||||||
|                     state.mapProperties.rasterLayer.mapD((layer) => { |  | ||||||
|                         const props = layer.properties |  | ||||||
|                         const attrUrl = props.attribution?.url |  | ||||||
|                         const attrText = props.attribution?.text |  | ||||||
| 
 |  | ||||||
|                         let bgAttr: BaseUIElement | string = undefined |  | ||||||
|                         if (attrText && attrUrl) { |  | ||||||
|                             bgAttr = |  | ||||||
|                                 "<a href='" + |  | ||||||
|                                 attrUrl + |  | ||||||
|                                 "' target='_blank' rel='noopener'>" + |  | ||||||
|                                 attrText + |  | ||||||
|                                 "</a>" |  | ||||||
|                         } else if (attrUrl) { |  | ||||||
|                             bgAttr = attrUrl |  | ||||||
|                         } else { |  | ||||||
|                             bgAttr = attrText |  | ||||||
|                         } |  | ||||||
|                         if (bgAttr) { |  | ||||||
|                             return Translations.t.general.attribution.attributionBackgroundLayerWithCopyright.Subs( |  | ||||||
|                                 { |  | ||||||
|                                     name: props.name, |  | ||||||
|                                     copyright: bgAttr, |  | ||||||
|                                 } |  | ||||||
|                             ) |  | ||||||
|                         } |  | ||||||
|                         return Translations.t.general.attribution.attributionBackgroundLayer.Subs( |  | ||||||
|                             props |  | ||||||
|                         ) |  | ||||||
|                     }) |  | ||||||
|                 ), |  | ||||||
| 
 |  | ||||||
|                 maintainer, |  | ||||||
|                 dataContributors, |  | ||||||
|                 CopyrightPanel.CodeContributors(contributors, t.codeContributionsBy), |  | ||||||
|                 CopyrightPanel.CodeContributors(translators, t.translatedBy), |  | ||||||
|                 new FixedUiElement("MapComplete " + Constants.vNumber).SetClass("font-bold"), |  | ||||||
|                 new Title(t.iconAttribution.title, 3), |  | ||||||
|                 ...iconAttributions, |  | ||||||
|             ].map((e) => e?.SetClass("mt-4")) |  | ||||||
|         ) |  | ||||||
|         this.SetClass("flex flex-col link-underline overflow-hidden") |  | ||||||
|         this.SetStyle("max-width:100%; width: 40rem; margin-left: 0.75rem; margin-right: 0.5rem") |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private static CodeContributors( |  | ||||||
|         contributors, |  | ||||||
|         translation: TypedTranslation<{ contributors; hiddenCount }> |  | ||||||
|     ): BaseUIElement { |  | ||||||
|         const total = contributors.contributors.length |  | ||||||
|         let filtered = [...contributors.contributors] |  | ||||||
| 
 |  | ||||||
|         filtered.splice(10, total - 10) |  | ||||||
| 
 |  | ||||||
|         let contribsStr = filtered.map((c) => c.contributor).join(", ") |  | ||||||
| 
 |  | ||||||
|         if (contribsStr === "") { |  | ||||||
|             // Hmm, something went wrong loading the contributors list. Lets show nothing
 |  | ||||||
|             return undefined |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return translation.Subs({ |  | ||||||
|             contributors: contribsStr, |  | ||||||
|             hiddenCount: total - 10, |  | ||||||
|         }) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private static IconAttribution(iconPath: string): BaseUIElement { |  | ||||||
|         if (iconPath.startsWith("http")) { |  | ||||||
|             try { |  | ||||||
|                 iconPath = "." + new URL(iconPath).pathname |  | ||||||
|             } catch (e) { |  | ||||||
|                 console.warn(e) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         const license: SmallLicense = CopyrightPanel.LicenseObject[iconPath] |  | ||||||
|         if (license == undefined) { |  | ||||||
|             return undefined |  | ||||||
|         } |  | ||||||
|         if (license.license.indexOf("trivial") >= 0) { |  | ||||||
|             return undefined |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         const sources = Utils.NoNull(Utils.NoEmpty(license.sources)) |  | ||||||
| 
 |  | ||||||
|         return new Combine([ |  | ||||||
|             new Img(iconPath).SetClass("w-12 min-h-12 mr-2 mb-2"), |  | ||||||
|             new Combine([ |  | ||||||
|                 new FixedUiElement(license.authors.join("; ")).SetClass("font-bold"), |  | ||||||
|                 license.license, |  | ||||||
|                 new Combine([ |  | ||||||
|                     ...sources.map((lnk) => { |  | ||||||
|                         let sourceLinkContent = lnk |  | ||||||
|                         try { |  | ||||||
|                             sourceLinkContent = new URL(lnk).hostname |  | ||||||
|                         } catch { |  | ||||||
|                             console.error("Not a valid URL:", lnk) |  | ||||||
|                         } |  | ||||||
|                         return new Link(sourceLinkContent, lnk, true).SetClass("mr-2 mb-2") |  | ||||||
|                     }), |  | ||||||
|                 ]).SetClass("flex flex-wrap"), |  | ||||||
|             ]) |  | ||||||
|                 .SetClass("flex flex-col") |  | ||||||
|                 .SetStyle("width: calc(100% - 50px - 0.5em); min-width: 12rem;"), |  | ||||||
|         ]).SetClass("flex flex-wrap border-b border-gray-300 m-2 border-box") |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private static GenerateLicenses() { |  | ||||||
|         const allLicenses = {} |  | ||||||
|         for (const key in licenses) { |  | ||||||
|             const license: SmallLicense = licenses[key] |  | ||||||
|             allLicenses[license.path] = license |  | ||||||
|         } |  | ||||||
|         return allLicenses |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										42
									
								
								src/UI/BigComponents/IconCopyrightPanel.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/UI/BigComponents/IconCopyrightPanel.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,42 @@ | ||||||
|  | <script lang="ts"> | ||||||
|  |   import type SmallLicense from "../../Models/smallLicense" | ||||||
|  |   import { Utils } from "../../Utils" | ||||||
|  |   import { twJoin } from "tailwind-merge" | ||||||
|  | 
 | ||||||
|  |   export let iconPath: string | ||||||
|  |   export let license: SmallLicense | ||||||
|  |   try { | ||||||
|  |     iconPath = "." + new URL(iconPath).pathname | ||||||
|  |   } catch (e) { | ||||||
|  |     console.warn(e) | ||||||
|  |   } | ||||||
|  |   let sources = Utils.NoNull(Utils.NoEmpty(license?.sources)) | ||||||
|  | 
 | ||||||
|  |   function sourceName(lnk: string) { | ||||||
|  |     try { | ||||||
|  |       return new URL(lnk).hostname | ||||||
|  |     } catch { | ||||||
|  |       console.error("Not a valid URL:", lnk) | ||||||
|  |     } | ||||||
|  |     return lnk | ||||||
|  |   } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | {#if license != undefined && license.license.indexOf("trivial") < 0} | ||||||
|  |   <div class="flex flex-wrap border-b border-gray-300 m-2 border-box"> | ||||||
|  |     <img class={twJoin( "w-12 min-h-12 mr-2 mb-2", license["mostly_white"] && "bg-slate-400 rounded-full h-12" )} | ||||||
|  |          src={iconPath} /> | ||||||
|  | 
 | ||||||
|  |     <div class="flex flex-col" style="width: calc(100% - 50px - 0.5em); min-width: 12rem;"> | ||||||
|  |       <div class="font-bold"> | ||||||
|  |         {license.authors.join("; ")} | ||||||
|  |       </div> | ||||||
|  |       {license.license} | ||||||
|  |       {#each sources as source} | ||||||
|  |         <a href={source} target="_blank">{sourceName(source)}</a> | ||||||
|  |       {/each} | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |   </div> | ||||||
|  | {/if} | ||||||
|  | 
 | ||||||
|  | @ -28,11 +28,10 @@ | ||||||
|   import UserRelatedState from "../Logic/State/UserRelatedState" |   import UserRelatedState from "../Logic/State/UserRelatedState" | ||||||
|   import LoginToggle from "./Base/LoginToggle.svelte" |   import LoginToggle from "./Base/LoginToggle.svelte" | ||||||
|   import LoginButton from "./Base/LoginButton.svelte" |   import LoginButton from "./Base/LoginButton.svelte" | ||||||
|   import CopyrightPanel from "./BigComponents/CopyrightPanel" |   import CopyrightPanel from "./BigComponents/CopyrightPanel.svelte" | ||||||
|   import DownloadPanel from "./DownloadFlow/DownloadPanel.svelte" |   import DownloadPanel from "./DownloadFlow/DownloadPanel.svelte" | ||||||
|   import ModalRight from "./Base/ModalRight.svelte" |   import ModalRight from "./Base/ModalRight.svelte" | ||||||
|   import LevelSelector from "./BigComponents/LevelSelector.svelte" |   import LevelSelector from "./BigComponents/LevelSelector.svelte" | ||||||
|   import SelectedElementTitle from "./BigComponents/SelectedElementTitle.svelte" |  | ||||||
|   import ThemeIntroPanel from "./BigComponents/ThemeIntroPanel.svelte" |   import ThemeIntroPanel from "./BigComponents/ThemeIntroPanel.svelte" | ||||||
|   import type { RasterLayerPolygon } from "../Models/RasterLayers" |   import type { RasterLayerPolygon } from "../Models/RasterLayers" | ||||||
|   import { AvailableRasterLayers } from "../Models/RasterLayers" |   import { AvailableRasterLayers } from "../Models/RasterLayers" | ||||||
|  | @ -526,7 +525,7 @@ | ||||||
|         </div> |         </div> | ||||||
| 
 | 
 | ||||||
|         <div slot="content2" class="m-2 flex flex-col"> |         <div slot="content2" class="m-2 flex flex-col"> | ||||||
|           <ToSvelte construct={() => new CopyrightPanel(state)} /> |           <CopyrightPanel {state}/> | ||||||
|         </div> |         </div> | ||||||
| 
 | 
 | ||||||
|         <div class="flex" slot="title3"> |         <div class="flex" slot="title3"> | ||||||
|  |  | ||||||
							
								
								
									
										21
									
								
								src/Utils.ts
									
										
									
									
									
								
							
							
						
						
									
										21
									
								
								src/Utils.ts
									
										
									
									
									
								
							|  | @ -1329,11 +1329,24 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | ||||||
|         return "#" + componentToHex(c.r) + componentToHex(c.g) + componentToHex(c.b) |         return "#" + componentToHex(c.r) + componentToHex(c.g) + componentToHex(c.b) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     private static percentageToNumber(v: string){ | ||||||
|  |         v = v.trim() | ||||||
|  |         if(v.endsWith("%")){ | ||||||
|  |             return Math.round((parseInt(v) * 255) / 100) | ||||||
|  |         } | ||||||
|  |         const n = Number(v) | ||||||
|  |         if(!isNaN(n)){ | ||||||
|  |             return n | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * |      * | ||||||
|      * Utils.color("#ff8000") // => {r: 255, g:128, b: 0}
 |      * Utils.color("#ff8000") // => {r: 255, g:128, b: 0}
 | ||||||
|      * Utils.color(" rgba  (12,34,56) ") // => {r: 12, g:34, b: 56}
 |      * Utils.color(" rgba  (12,34,56) ") // => {r: 12, g:34, b: 56}
 | ||||||
|      * Utils.color(" rgba  (12,34,56,0.5) ") // => {r: 12, g:34, b: 56}
 |      * Utils.color(" rgba  (12,34,56,0.5) ") // => {r: 12, g:34, b: 56}
 | ||||||
|  |      * Utils.color("rgb(100%,100%,100%)") // => {r: 255, g: 255, b: 255}
 | ||||||
|      * Utils.color(undefined) // => undefined
 |      * Utils.color(undefined) // => undefined
 | ||||||
|      */ |      */ | ||||||
|     public static color(hex: string): { r: number; g: number; b: number } { |     public static color(hex: string): { r: number; g: number; b: number } { | ||||||
|  | @ -1341,14 +1354,16 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | ||||||
|             return undefined |             return undefined | ||||||
|         } |         } | ||||||
|         hex = hex.replace(/[ \t]/g, "") |         hex = hex.replace(/[ \t]/g, "") | ||||||
|         if (hex.startsWith("rgba(")) { |         if (hex.startsWith("rgba(") || hex.startsWith("rgb(")) { | ||||||
|             const match = hex.match(/rgba\(([0-9.]+),([0-9.]+),([0-9.]+)(,[0-9.]*)?\)/) |             const match = hex.match(/rgba?\(([0-9.]+%?),([0-9.]+%?),([0-9.]+%?)(,[0-9.]+%?)?\)/); | ||||||
|             if (match == undefined) { |             if (match == undefined) { | ||||||
|                 return undefined |                 return undefined | ||||||
|             } |             } | ||||||
|             return { r: Number(match[1]), g: Number(match[2]), b: Number(match[3]) } | 
 | ||||||
|  |             return { r: Utils.percentageToNumber (match[1]), g: Utils.percentageToNumber (match[2]), b: Utils.percentageToNumber (match[3]) } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|         if (!hex.startsWith("#")) { |         if (!hex.startsWith("#")) { | ||||||
|             return undefined |             return undefined | ||||||
|         } |         } | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue