diff --git a/public/css/index-tailwind-output.css b/public/css/index-tailwind-output.css index 46d027e00..6370c0761 100644 --- a/public/css/index-tailwind-output.css +++ b/public/css/index-tailwind-output.css @@ -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)); diff --git a/scripts/ScriptUtils.ts b/scripts/ScriptUtils.ts index edf09f7ed..6bc78d7f8 100644 --- a/scripts/ScriptUtils.ts +++ b/scripts/ScriptUtils.ts @@ -123,7 +123,7 @@ export default class ScriptUtils { return ScriptUtils.DownloadJSON(url) } - public static async ReadSvg(path: string): Promise { + public static async ReadSvg(path: string): Promise { if (!existsSync(path)) { throw "File not found: " + path } diff --git a/scripts/generateLicenseInfo.ts b/scripts/generateLicenseInfo.ts index e8d92bd04..434a73c01 100644 --- a/scripts/generateLicenseInfo.ts +++ b/scripts/generateLicenseInfo.ts @@ -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() + for (const icon of allIcons) { + if (!icon.endsWith(".svg")) { + continue + } + + const svg = await ScriptUtils.ReadSvg(icon) + + const colours = new Set() + 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 = 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 diff --git a/src/Models/smallLicense.ts b/src/Models/smallLicense.ts index f495c236e..ab78c2ee4 100644 --- a/src/Models/smallLicense.ts +++ b/src/Models/smallLicense.ts @@ -3,4 +3,5 @@ export default interface SmallLicense { authors: string[] license: string sources: string[] + mostly_white?: boolean } diff --git a/src/UI/BigComponents/CopyrightPanel.svelte b/src/UI/BigComponents/CopyrightPanel.svelte new file mode 100644 index 000000000..42c9173f5 --- /dev/null +++ b/src/UI/BigComponents/CopyrightPanel.svelte @@ -0,0 +1,188 @@ + + + diff --git a/src/UI/BigComponents/CopyrightPanel.ts b/src/UI/BigComponents/CopyrightPanel.ts deleted file mode 100644 index deba888f8..000000000 --- a/src/UI/BigComponents/CopyrightPanel.ts +++ /dev/null @@ -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 - readonly rasterLayer: Store - } - osmConnection: OsmConnection - dataIsLoading: Store - perLayer: ReadonlyMap - }) { - 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) => - `${kv.name}` - ) - 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 = - "" + - attrText + - "" - } 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 - } -} diff --git a/src/UI/BigComponents/IconCopyrightPanel.svelte b/src/UI/BigComponents/IconCopyrightPanel.svelte new file mode 100644 index 000000000..ad539238f --- /dev/null +++ b/src/UI/BigComponents/IconCopyrightPanel.svelte @@ -0,0 +1,42 @@ + + +{#if license != undefined && license.license.indexOf("trivial") < 0} +
+ + +
+
+ {license.authors.join("; ")} +
+ {license.license} + {#each sources as source} + {sourceName(source)} + {/each} +
+ +
+{/if} + diff --git a/src/UI/ThemeViewGUI.svelte b/src/UI/ThemeViewGUI.svelte index 26a24bb77..b2980971d 100644 --- a/src/UI/ThemeViewGUI.svelte +++ b/src/UI/ThemeViewGUI.svelte @@ -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 @@
- new CopyrightPanel(state)} /> +
diff --git a/src/Utils.ts b/src/Utils.ts index 7d064ebd3..c0da44571 100644 --- a/src/Utils.ts +++ b/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 }