import Combine from "../Base/Combine"; import Translations from "../i18n/Translations"; import {Store, UIEventSource} from "../../Logic/UIEventSource"; import {FixedUiElement} from "../Base/FixedUiElement"; import * as 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 * as contributors from "../../assets/contributors.json" import * as translators from "../../assets/translators.json" import BaseUIElement from "../BaseUIElement"; import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; import Title from "../Base/Title"; import {SubtleButton} from "../Base/SubtleButton"; import Svg from "../../Svg"; import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"; import {BBox} from "../../Logic/BBox"; import Loc from "../../Models/Loc"; import Toggle from "../Input/Toggle"; 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 TranslatorsPanel from "./TranslatorsPanel"; import {MapillaryLink} from "./MapillaryLink"; import FullWelcomePaneWithTabs from "./FullWelcomePaneWithTabs"; export class OpenIdEditor extends VariableUiElement { constructor(state: { locationControl: UIEventSource }, iconStyle?: string, objectId?: string) { const t = Translations.t.general.attribution super(state.locationControl.map(location => { let elementSelect = ""; if (objectId !== undefined) { const parts = objectId.split("/") const tp = parts[0] if (parts.length === 2 && !isNaN(Number(parts[1])) && (tp === "node" || tp === "way" || tp === "relation")) { elementSelect = "&" + tp + "=" + parts[1] } } const idLink = `https://www.openstreetmap.org/edit?editor=id${elementSelect}#map=${location?.zoom ?? 0}/${location?.lat ?? 0}/${location?.lon ?? 0}` return new SubtleButton(Svg.pencil_ui().SetStyle(iconStyle), t.editId, {url: idLink, newTab: true}) })); } } export class OpenJosm extends Combine { constructor(state: { osmConnection: OsmConnection, currentBounds: Store, }, iconStyle?: string) { const t = Translations.t.general.attribution const josmState = new UIEventSource(undefined) // Reset after 15s josmState.stabilized(15000).addCallbackD(_ => josmState.setData(undefined)) const stateIndication = new VariableUiElement(josmState.map(state => { if (state === undefined) { return undefined } state = state.toUpperCase() if (state === "OK") { return t.josmOpened.SetClass("thanks") } return t.josmNotOpened.SetClass("alert") })); const toggle = new Toggle( new SubtleButton(Svg.josm_logo_ui().SetStyle(iconStyle), t.editJosm).onClick(() => { const bounds: any = state.currentBounds.data; if (bounds === undefined) { return undefined } const top = bounds.getNorth(); const bottom = bounds.getSouth(); const right = bounds.getEast(); const left = bounds.getWest(); const josmLink = `http://127.0.0.1:8111/load_and_zoom?left=${left}&right=${right}&top=${top}&bottom=${bottom}` Utils.download(josmLink).then(answer => josmState.setData(answer.replace(/\n/g, '').trim())).catch(_ => josmState.setData("ERROR")) }), undefined, state.osmConnection.userDetails.map(ud => ud.loggedIn && ud.csCount >= Constants.userJourney.historyLinkVisible)) super([stateIndication, toggle]); } } /** * The attribution panel shown on mobile */ export default class CopyrightPanel extends Combine { private static LicenseObject = CopyrightPanel.GenerateLicenses(); constructor(state: { layoutToUse: LayoutConfig, featurePipeline: FeaturePipeline, currentBounds: Store, locationControl: UIEventSource, osmConnection: OsmConnection, isTranslator: Store }) { const t = Translations.t.general.attribution const layoutToUse = state.layoutToUse const imgSize = "h-6 w-6" const iconStyle = "height: 1.5rem; width: 1.5rem" const actionButtons = [ new SubtleButton(Svg.liberapay_ui(), t.donate, { url: "https://liberapay.com/pietervdvn/", newTab: true, imgSize }), new SubtleButton(Svg.bug_ui(), t.openIssueTracker, { url: "https://github.com/pietervdvn/MapComplete/issues", newTab: true, imgSize }), new SubtleButton(Svg.statistics_ui(), t.openOsmcha.Subs({theme: state.layoutToUse.title}), { url: Utils.OsmChaLinkFor(31, state.layoutToUse.id), newTab: true, imgSize }), new SubtleButton(Svg.mastodon_ui(), new Combine([t.followOnMastodon.SetClass("font-bold"), t.followBridge]).SetClass("flex flex-col"), { url:"https://en.osm.town/web/notifications", newTab: true, imgSize }), new SubtleButton(Svg.twitter_ui(), t.followOnTwitter, { url:"https://twitter.com/mapcomplete", newTab: true, imgSize }), new OpenIdEditor(state, iconStyle), new MapillaryLink(state, iconStyle), new OpenJosm(state, iconStyle), new TranslatorsPanel(state, iconStyle) ] const iconAttributions = layoutToUse.usedImages.map(CopyrightPanel.IconAttribution) let maintainer: BaseUIElement = undefined if (layoutToUse.maintainer !== undefined && layoutToUse.maintainer !== "" && layoutToUse.maintainer.toLowerCase() !== "mapcomplete") { maintainer = t.themeBy.Subs({author: layoutToUse.maintainer}) } 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, maintainer, new FixedUiElement(layoutToUse.credits), dataContributors, CopyrightPanel.CodeContributors(contributors, t.codeContributionsBy), CopyrightPanel.CodeContributors(translators, t.translatedBy), new FixedUiElement("MapComplete " + Constants.vNumber).SetClass("font-bold"), new Combine(actionButtons).SetClass("block w-full link-no-underline"), 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; } }