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
				
			
		
							
								
								
									
										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} | ||||
| 
 | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue