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)); | ||||
| } | ||||
| 
 | ||||
| .bg-slate-400 { | ||||
|   --tw-bg-opacity: 1; | ||||
|   background-color: rgb(148 163 184 / var(--tw-bg-opacity)); | ||||
| } | ||||
| 
 | ||||
| .bg-black { | ||||
|   --tw-bg-opacity: 1; | ||||
|   background-color: rgb(0 0 0 / var(--tw-bg-opacity)); | ||||
|  |  | |||
|  | @ -123,7 +123,7 @@ export default class ScriptUtils { | |||
|         return ScriptUtils.DownloadJSON(url) | ||||
|     } | ||||
| 
 | ||||
|     public static async ReadSvg(path: string): Promise<any> { | ||||
|     public static async ReadSvg(path: string): Promise<SVGElement> { | ||||
|         if (!existsSync(path)) { | ||||
|             throw "File not found: " + path | ||||
|         } | ||||
|  |  | |||
|  | @ -3,7 +3,9 @@ import SmallLicense from "../src/Models/smallLicense" | |||
| import ScriptUtils from "./ScriptUtils" | ||||
| import Script from "./Script" | ||||
| import { Utils } from "../src/Utils" | ||||
| 
 | ||||
| const prompt = require("prompt-sync")() | ||||
| 
 | ||||
| export class GenerateLicenseInfo extends Script { | ||||
|     private static readonly needsLicenseRef = new Set( | ||||
|         ScriptUtils.readDirRecSync("./LICENSES") | ||||
|  | @ -23,7 +25,7 @@ export class GenerateLicenseInfo extends Script { | |||
|             authors: ["Pieter Vander Vennet"], | ||||
|             path: undefined, | ||||
|             license: "CC0", | ||||
|             sources: [], | ||||
|             sources: [] | ||||
|         }) | ||||
|         knownLicenses.set("streetcomplete", { | ||||
|             authors: ["Tobias Zwick (westnordost)"], | ||||
|  | @ -31,8 +33,8 @@ export class GenerateLicenseInfo extends Script { | |||
|             license: "CC0", | ||||
|             sources: [ | ||||
|                 "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", { | ||||
|  | @ -41,34 +43,34 @@ export class GenerateLicenseInfo extends Script { | |||
|             license: "CC0", | ||||
|             sources: [ | ||||
|                 "https://github.com/ideditor/temaki", | ||||
|                 "https://ideditor.github.io/temaki/docs/", | ||||
|             ], | ||||
|                 "https://ideditor.github.io/temaki/docs/" | ||||
|             ] | ||||
|         }) | ||||
| 
 | ||||
|         knownLicenses.set("maki", { | ||||
|             authors: ["Maki"], | ||||
|             path: undefined, | ||||
|             license: "CC0", | ||||
|             sources: ["https://labs.mapbox.com/maki-icons/"], | ||||
|             sources: ["https://labs.mapbox.com/maki-icons/"] | ||||
|         }) | ||||
| 
 | ||||
|         knownLicenses.set("t", { | ||||
|             authors: [], | ||||
|             path: undefined, | ||||
|             license: "CC0; trivial", | ||||
|             sources: [], | ||||
|             sources: [] | ||||
|         }) | ||||
|         knownLicenses.set("na", { | ||||
|             authors: [], | ||||
|             path: undefined, | ||||
|             license: "CC0", | ||||
|             sources: [], | ||||
|             sources: [] | ||||
|         }) | ||||
|         knownLicenses.set("carto", { | ||||
|             authors: ["OSM-Carto"], | ||||
|             path: undefined, | ||||
|             license: "CC0", | ||||
|             sources: [""], | ||||
|             sources: [""] | ||||
|         }) | ||||
|         knownLicenses.set("tv", { | ||||
|             authors: ["Toerisme Vlaanderen"], | ||||
|  | @ -76,20 +78,20 @@ export class GenerateLicenseInfo extends Script { | |||
|             license: "CC0", | ||||
|             sources: [ | ||||
|                 "https://toerismevlaanderen.be/pinjepunt", | ||||
|                 "https://mapcomplete.org/toerisme_vlaanderenn", | ||||
|             ], | ||||
|                 "https://mapcomplete.org/toerisme_vlaanderenn" | ||||
|             ] | ||||
|         }) | ||||
|         knownLicenses.set("tvf", { | ||||
|             authors: ["Jo De Baerdemaeker "], | ||||
|             path: undefined, | ||||
|             license: "All rights reserved", | ||||
|             sources: ["https://www.studiotype.be/fonts/flandersart"], | ||||
|             sources: ["https://www.studiotype.be/fonts/flandersart"] | ||||
|         }) | ||||
|         knownLicenses.set("twemoji", { | ||||
|             authors: ["Twemoji"], | ||||
|             path: undefined, | ||||
|             license: "CC-BY 4.0", | ||||
|             sources: ["https://github.com/twitter/twemoji"], | ||||
|             sources: ["https://github.com/twitter/twemoji"] | ||||
|         }) | ||||
|         return knownLicenses | ||||
|     } | ||||
|  | @ -135,6 +137,50 @@ export class GenerateLicenseInfo extends Script { | |||
|         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[]) { | ||||
|         const missing = [] | ||||
| 
 | ||||
|  | @ -182,7 +228,7 @@ export class GenerateLicenseInfo extends Script { | |||
|             authors: author.split(";"), | ||||
|             path: 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", | ||||
|             "LOGO-BY-THE-GOVERNMENT": "LOGO", | ||||
|             PD: "PUBLIC-DOMAIN", | ||||
|             "LOGO-(ALL-RIGHTS-RESERVED)": "LOGO", | ||||
|             "LOGO-(ALL-RIGHTS-RESERVED)": "LOGO" | ||||
|             /*  ALL-RIGHTS-RESERVED: | ||||
|             PD: | ||||
|                 PUBLIC-DOMAIN: | ||||
|  | @ -246,7 +292,7 @@ export class GenerateLicenseInfo extends Script { | |||
|                 path: license.path, | ||||
|                 license: license.license, | ||||
|                 authors: license.authors, | ||||
|                 sources: license.sources, | ||||
|                 sources: license.sources | ||||
|             } | ||||
| 
 | ||||
|             cloned.license = Utils.Dedup( | ||||
|  | @ -289,7 +335,7 @@ export class GenerateLicenseInfo extends Script { | |||
|     } | ||||
| 
 | ||||
|     queryMissingLicenses(missingLicenses: string[]) { | ||||
|         process.on("SIGINT", function () { | ||||
|         process.on("SIGINT", function() { | ||||
|             console.log("Aborting... Bye!") | ||||
|             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 | ||||
|      * @param licensePaths | ||||
|      */ | ||||
|     createFullLicenseOverview(licensePaths: string[]) { | ||||
|     createFullLicenseOverview(licensePaths: string[], mostlyWhite: string[]) { | ||||
|         const allLicenses: SmallLicense[] = [] | ||||
|         for (const licensePath of licensePaths) { | ||||
|             if (!existsSync(licensePath)) { | ||||
|  | @ -327,6 +373,9 @@ export class GenerateLicenseInfo extends Script { | |||
|                     licensePath.length - "license_info.json".length | ||||
|                 ) | ||||
|                 license.path = dir + license.path | ||||
|                 if(mostlyWhite.some(l => license.path === l)){ | ||||
|                     license["mostly_white"] = true | ||||
|                 } | ||||
|                 allLicenses.push(license) | ||||
|             } | ||||
|         } | ||||
|  | @ -354,6 +403,7 @@ export class GenerateLicenseInfo extends Script { | |||
|             (pth) => pth.match(/(.svg|.png|.jpg|.ttf|.otf|.woff|.jpeg)$/i) != null | ||||
|         ) | ||||
|         const missingLicenses = this.missingLicenseInfos(licenseInfos, artwork) | ||||
|         const mostlyWhite: Set<string> = await this.mostlyWhite(artwork) | ||||
|         if (args.indexOf("--prompt") >= 0 || args.indexOf("--query") >= 0) { | ||||
|             this.queryMissingLicenses(missingLicenses) | ||||
|             return this.main([]) | ||||
|  | @ -371,7 +421,7 @@ export class GenerateLicenseInfo extends Script { | |||
|             if (licenseInfo.sources.length + licenseInfo.authors.length == 0 && !isTrivial) { | ||||
|                 invalidLicenses.push( | ||||
|                     "Invalid license: No sources nor authors given in the license for " + | ||||
|                         JSON.stringify(licenseInfo) | ||||
|                     JSON.stringify(licenseInfo) | ||||
|                 ) | ||||
|                 continue | ||||
|             } | ||||
|  | @ -394,10 +444,10 @@ export class GenerateLicenseInfo extends Script { | |||
|             const spdxContent = [ | ||||
|                 "SPDX-FileCopyrightText: " + licenseInfo.authors.join("; "), | ||||
|                 "SPDX-License-Identifier: " + | ||||
|                     licenseInfo.license | ||||
|                         .split(" AND ") | ||||
|                         .map((s) => this.addLicenseRef(s)) | ||||
|                         .join(" AND "), | ||||
|                 licenseInfo.license | ||||
|                     .split(" AND ") | ||||
|                     .map((s) => this.addLicenseRef(s)) | ||||
|                     .join(" AND ") | ||||
|             ] | ||||
|             writeFileSync(spdxPath, spdxContent.join("\n")) | ||||
|         } | ||||
|  | @ -412,7 +462,7 @@ export class GenerateLicenseInfo extends Script { | |||
|         } | ||||
| 
 | ||||
|         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 { | ||||
|         if (GenerateLicenseInfo.needsLicenseRef.has(s)) { | ||||
|             console.log("Mapping ", s, Array.from(GenerateLicenseInfo.needsLicenseRef)) | ||||
|             return "LicenseRef-" + s | ||||
|         } | ||||
|         return s | ||||
|  |  | |||
|  | @ -3,4 +3,5 @@ export default interface SmallLicense { | |||
|     authors: string[] | ||||
|     license: 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 LoginToggle from "./Base/LoginToggle.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 ModalRight from "./Base/ModalRight.svelte" | ||||
|   import LevelSelector from "./BigComponents/LevelSelector.svelte" | ||||
|   import SelectedElementTitle from "./BigComponents/SelectedElementTitle.svelte" | ||||
|   import ThemeIntroPanel from "./BigComponents/ThemeIntroPanel.svelte" | ||||
|   import type { RasterLayerPolygon } from "../Models/RasterLayers" | ||||
|   import { AvailableRasterLayers } from "../Models/RasterLayers" | ||||
|  | @ -526,7 +525,7 @@ | |||
|         </div> | ||||
| 
 | ||||
|         <div slot="content2" class="m-2 flex flex-col"> | ||||
|           <ToSvelte construct={() => new CopyrightPanel(state)} /> | ||||
|           <CopyrightPanel {state}/> | ||||
|         </div> | ||||
| 
 | ||||
|         <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) | ||||
|     } | ||||
| 
 | ||||
|     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(" 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("rgb(100%,100%,100%)") // => {r: 255, g: 255, b: 255}
 | ||||
|      * Utils.color(undefined) // => undefined
 | ||||
|      */ | ||||
|     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 | ||||
|         } | ||||
|         hex = hex.replace(/[ \t]/g, "") | ||||
|         if (hex.startsWith("rgba(")) { | ||||
|             const match = hex.match(/rgba\(([0-9.]+),([0-9.]+),([0-9.]+)(,[0-9.]*)?\)/) | ||||
|         if (hex.startsWith("rgba(") || hex.startsWith("rgb(")) { | ||||
|             const match = hex.match(/rgba?\(([0-9.]+%?),([0-9.]+%?),([0-9.]+%?)(,[0-9.]+%?)?\)/); | ||||
|             if (match == 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("#")) { | ||||
|             return undefined | ||||
|         } | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue