Themes: add possibility to add an icon to 'render' (just like with mappings), add contact:mastodon support as general question, add mastodon question to hackerspaces

This commit is contained in:
Pieter Vander Vennet 2023-08-10 14:10:06 +02:00
parent 64648f7bb4
commit 03aafbe99c
8 changed files with 237 additions and 99 deletions

View file

@ -112,6 +112,7 @@
"website", "website",
"email", "email",
"phone", "phone",
"mastodon",
{ {
"builtin": "opening_hours_24_7", "builtin": "opening_hours_24_7",
"override": { "override": {

View file

@ -173,11 +173,13 @@
"render": { "render": {
"*": "<a href='tel:{phone}'>{phone}</a>" "*": "<a href='tel:{phone}'>{phone}</a>"
}, },
"icon": "./assets/layers/questions/phone.svg",
"mappings": [ "mappings": [
{ {
"if": "contact:phone~*", "if": "contact:phone~*",
"then": "<a href='tel:{contact:phone}'>{contact:phone}</a>", "then": "<a href='tel:{contact:phone}'>{contact:phone}</a>",
"hideInAnswer": true "hideInAnswer": true,
"icon": "./assets/layers/questions/phone.svg"
} }
], ],
"freeform": { "freeform": {
@ -188,6 +190,21 @@
] ]
} }
}, },
{
"id": "mastodon",
"description": "Shows and asks for the mastodon handle",
"question": {
"en": "What is the Mastodon-handle of {title()}?"
},
"freeform": {
"key": "contact:mastodon",
"type": "fediverse"
},
"render": {
"*": "{fediverse_link(contact:mastodon)}"
},
"icon": "./assets/svg/mastodon.svg"
},
{ {
"id": "osmlink", "id": "osmlink",
"render": { "render": {
@ -205,6 +222,7 @@
"render": { "render": {
"*": "<a href='mailto:{email}' target='_blank'>{email}</a>" "*": "<a href='mailto:{email}' target='_blank'>{email}</a>"
}, },
"icon": "./assets/svg/envelope.svg",
"labels": [ "labels": [
"contact" "contact"
], ],
@ -236,6 +254,7 @@
"mappings": [ "mappings": [
{ {
"if": "contact:email~*", "if": "contact:email~*",
"icon": "./assets/svg/envelope.svg",
"then": "<a href='mailto:{contact:email}' target='_blank'>{contact:email}</a>", "then": "<a href='mailto:{contact:email}' target='_blank'>{contact:email}</a>",
"hideInAnswer": true "hideInAnswer": true
} }
@ -253,6 +272,7 @@
"labels": [ "labels": [
"contact" "contact"
], ],
"icon": "./assets/layers/icons/website.svg",
"question": { "question": {
"en": "What is the website of {title()}?", "en": "What is the website of {title()}?",
"nl": "Wat is de website van {title()}?", "nl": "Wat is de website van {title()}?",
@ -292,7 +312,8 @@
{ {
"if": "contact:website~*", "if": "contact:website~*",
"then": "<a href='{contact:website}' rel='nofollow noopener noreferrer' target='_blank'>{contact:website}</a>", "then": "<a href='{contact:website}' rel='nofollow noopener noreferrer' target='_blank'>{contact:website}</a>",
"hideInAnswer": true "hideInAnswer": true,
"icon": "./assets/layers/icons/website.svg"
} }
] ]
}, },
@ -2203,4 +2224,4 @@
] ]
} }
] ]
} }

View file

@ -610,6 +610,11 @@
"feedback": "This is not a valid email address", "feedback": "This is not a valid email address",
"noAt": "An e-mail address must contain an @" "noAt": "An e-mail address must contain an @"
}, },
"fediverse": {
"description": "A fediverse handle, often @username@server.tld",
"feedback": "A fediverse handle consists of @username@server.tld or is a link to a profile",
"invalidHost": "{host} is not a valid hostname"
},
"float": { "float": {
"description": "a number", "description": "a number",
"feedback": "This is not a number" "feedback": "This is not a number"

View file

@ -41,6 +41,26 @@ export interface TagRenderingConfigJson {
| Record<string, string> | Record<string, string>
| { special: Record<string, string | Record<string, string>> & { type: string } } | { special: Record<string, string | Record<string, string>> & { type: string } }
/**
* An icon shown next to the rendering; typically shown pretty small
* This is only shown next to the "render" value
* Type: icon
*/
icon?:
| string
| {
/**
* The path to the icon
* Type: icon
*/
path: string
/**
* A hint to mapcomplete on how to render this icon within the mapping.
* This is translated to 'mapping-icon-<classtype>', so defining your own in combination with a custom CSS is possible (but discouraged)
*/
class?: "small" | "medium" | "large" | string
}
/** /**
* Only show this tagrendering (or ask the question) if the selected object also matches the tags specified as `condition`. * Only show this tagrendering (or ask the question) if the selected object also matches the tags specified as `condition`.
* *

View file

@ -1,23 +1,24 @@
import { Translation, TypedTranslation } from "../../UI/i18n/Translation" import {Translation, TypedTranslation} from "../../UI/i18n/Translation"
import { TagsFilter } from "../../Logic/Tags/TagsFilter" import {TagsFilter} from "../../Logic/Tags/TagsFilter"
import Translations from "../../UI/i18n/Translations" import Translations from "../../UI/i18n/Translations"
import { TagUtils, UploadableTag } from "../../Logic/Tags/TagUtils" import {TagUtils, UploadableTag} from "../../Logic/Tags/TagUtils"
import { And } from "../../Logic/Tags/And" import {And} from "../../Logic/Tags/And"
import { Utils } from "../../Utils" import {Utils} from "../../Utils"
import { Tag } from "../../Logic/Tags/Tag" import {Tag} from "../../Logic/Tags/Tag"
import BaseUIElement from "../../UI/BaseUIElement" import BaseUIElement from "../../UI/BaseUIElement"
import Combine from "../../UI/Base/Combine" import Combine from "../../UI/Base/Combine"
import Title from "../../UI/Base/Title" import Title from "../../UI/Base/Title"
import Link from "../../UI/Base/Link" import Link from "../../UI/Base/Link"
import List from "../../UI/Base/List" import List from "../../UI/Base/List"
import { import {MappingConfigJson, QuestionableTagRenderingConfigJson,} from "./Json/QuestionableTagRenderingConfigJson"
MappingConfigJson, import {FixedUiElement} from "../../UI/Base/FixedUiElement"
QuestionableTagRenderingConfigJson, import {Paragraph} from "../../UI/Base/Paragraph"
} from "./Json/QuestionableTagRenderingConfigJson"
import { FixedUiElement } from "../../UI/Base/FixedUiElement"
import { Paragraph } from "../../UI/Base/Paragraph"
import Svg from "../../Svg" import Svg from "../../Svg"
import Validators, { ValidatorType } from "../../UI/InputElement/Validators" import Validators, {ValidatorType} from "../../UI/InputElement/Validators"
export interface Icon {
}
export interface Mapping { export interface Mapping {
readonly if: UploadableTag readonly if: UploadableTag
@ -45,6 +46,8 @@ export interface Mapping {
export default class TagRenderingConfig { export default class TagRenderingConfig {
public readonly id: string public readonly id: string
public readonly render?: TypedTranslation<object> public readonly render?: TypedTranslation<object>
public readonly renderIcon?: string
public readonly renderIconClass?: string
public readonly question?: TypedTranslation<object> public readonly question?: TypedTranslation<object>
public readonly questionhint?: TypedTranslation<object> public readonly questionhint?: TypedTranslation<object>
public readonly condition?: TagsFilter public readonly condition?: TagsFilter
@ -121,9 +124,16 @@ export default class TagRenderingConfig {
this.question = Translations.T(json.question, translationKey + ".question") this.question = Translations.T(json.question, translationKey + ".question")
this.questionhint = Translations.T(json.questionHint, translationKey + ".questionHint") this.questionhint = Translations.T(json.questionHint, translationKey + ".questionHint")
this.description = Translations.T(json.description, translationKey + ".description") this.description = Translations.T(json.description, translationKey + ".description")
this.condition = TagUtils.Tag(json.condition ?? { and: [] }, `${context}.condition`) this.condition = TagUtils.Tag(json.condition ?? {and: []}, `${context}.condition`)
if (typeof json.icon === "string") {
this.renderIcon = json.icon
this.renderIconClass = "small"
}else if (typeof json.icon === "object"){
this.renderIcon = json.icon.path
this.renderIconClass = json.icon.class
}
this.metacondition = TagUtils.Tag( this.metacondition = TagUtils.Tag(
json.metacondition ?? { and: [] }, json.metacondition ?? {and: []},
`${context}.metacondition` `${context}.metacondition`
) )
if (json.freeform) { if (json.freeform) {
@ -238,15 +248,17 @@ export default class TagRenderingConfig {
if (txt.indexOf("{" + this.freeform.key + ":") >= 0) { if (txt.indexOf("{" + this.freeform.key + ":") >= 0) {
continue continue
} }
if (txt.indexOf("{canonical(" + this.freeform.key + ")") >= 0) {
continue
}
if ( if (
this.freeform.type === "opening_hours" && this.freeform.type === "opening_hours" &&
txt.indexOf("{opening_hours_table(") >= 0 txt.indexOf("{opening_hours_table(") >= 0
) { ) {
continue continue
} }
const keyFirstArg = ["canonical", "fediverse_link"]
if (keyFirstArg.some(funcName => txt.indexOf(`{${funcName}(${this.freeform.key}`) >= 0)) {
continue
}
if ( if (
this.freeform.type === "wikidata" && this.freeform.type === "wikidata" &&
txt.indexOf("{wikipedia(" + this.freeform.key) >= 0 txt.indexOf("{wikipedia(" + this.freeform.key) >= 0
@ -532,7 +544,7 @@ export default class TagRenderingConfig {
*/ */
public GetRenderValueWithImage( public GetRenderValueWithImage(
tags: Record<string, string> tags: Record<string, string>
): { then: TypedTranslation<any>; icon?: string } | undefined { ): { then: TypedTranslation<any>; icon?: string, iconClass?: string } | undefined {
if (this.condition !== undefined) { if (this.condition !== undefined) {
if (!this.condition.matchesProperties(tags)) { if (!this.condition.matchesProperties(tags)) {
return undefined return undefined
@ -551,7 +563,7 @@ export default class TagRenderingConfig {
} }
if (this.freeform?.key === undefined || tags[this.freeform.key] !== undefined) { if (this.freeform?.key === undefined || tags[this.freeform.key] !== undefined) {
return { then: this.render } return {then: this.render, icon: this.renderIcon, iconClass: this.renderIconClass}
} }
return undefined return undefined
@ -773,7 +785,7 @@ export default class TagRenderingConfig {
if (m.ifnot !== undefined) { if (m.ifnot !== undefined) {
msgs.push( msgs.push(
"Unselecting this answer will add " + "Unselecting this answer will add " +
m.ifnot.asHumanString(true, false, {}) m.ifnot.asHumanString(true, false, {})
) )
} }
return msgs return msgs
@ -803,12 +815,12 @@ export default class TagRenderingConfig {
this.description, this.description,
this.question !== undefined this.question !== undefined
? new Combine([ ? new Combine([
"The question is ", "The question is ",
new FixedUiElement(this.question.txt).SetClass("font-bold bold"), new FixedUiElement(this.question.txt).SetClass("font-bold bold"),
]) ])
: new FixedUiElement( : new FixedUiElement(
"This tagrendering has no question and is thus read-only" "This tagrendering has no question and is thus read-only"
).SetClass("italic"), ).SetClass("italic"),
new Combine(withRender), new Combine(withRender),
mappings, mappings,
condition, condition,

View file

@ -18,6 +18,7 @@ import ColorValidator from "./Validators/ColorValidator"
import BaseUIElement from "../BaseUIElement" import BaseUIElement from "../BaseUIElement"
import Combine from "../Base/Combine" import Combine from "../Base/Combine"
import Title from "../Base/Title" import Title from "../Base/Title"
import FediverseValidator from "./Validators/FediverseValidator";
export type ValidatorType = (typeof Validators.availableTypes)[number] export type ValidatorType = (typeof Validators.availableTypes)[number]
@ -39,6 +40,7 @@ export default class Validators {
"phone", "phone",
"opening_hours", "opening_hours",
"color", "color",
"fediverse"
] as const ] as const
public static readonly AllValidators: ReadonlyArray<Validator> = [ public static readonly AllValidators: ReadonlyArray<Validator> = [
@ -58,6 +60,7 @@ export default class Validators {
new PhoneValidator(), new PhoneValidator(),
new OpeningHoursValidator(), new OpeningHoursValidator(),
new ColorValidator(), new ColorValidator(),
new FediverseValidator()
] ]
private static _byType = Validators._byTypeConstructor() private static _byType = Validators._byTypeConstructor()

View file

@ -0,0 +1,63 @@
import {Validator} from "../Validator"
import {Translation} from "../../i18n/Translation";
import Translations from "../../i18n/Translations";
export default class FediverseValidator extends Validator {
public static readonly usernameAtServer: RegExp = /^@?(\w+)@((\w|\.)+)$/
constructor() {
super("fediverse", "Validates fediverse addresses and normalizes them into `@username@server`-format");
}
/**
* Returns an `@username@host`
* @param s
*/
reformat(s: string): string {
if(!s.startsWith("@")){
s = "@"+s
}
if (s.match(FediverseValidator.usernameAtServer)) {
return s
}
try {
const url = new URL(s)
const path = url.pathname
if (path.match(/^\/\w+$/)) {
return `@${path.substring(1)}@${url.hostname}`;
}
} catch (e) {
// Nothing to do here
}
return undefined
}
getFeedback(s: string): Translation | undefined {
const match = s.match(FediverseValidator.usernameAtServer)
console.log("Match:", match)
if (match) {
const host = match[2]
try {
const url = new URL("https://" + host)
return undefined
} catch (e) {
return Translations.t.validation.fediverse.invalidHost.Subs({host})
}
}
try {
const url = new URL(s)
const path = url.pathname
if (path.match(/^\/\w+$/)) {
return undefined
}
} catch (e) {
// Nothing to do here
}
return Translations.t.validation.fediverse.feedback
}
isValid(s): boolean {
return this.getFeedback(s) === undefined
}
}

View file

@ -1,56 +1,52 @@
import Combine from "./Base/Combine" import Combine from "./Base/Combine"
import { FixedUiElement } from "./Base/FixedUiElement" import {FixedUiElement} from "./Base/FixedUiElement"
import BaseUIElement from "./BaseUIElement" import BaseUIElement from "./BaseUIElement"
import Title from "./Base/Title" import Title from "./Base/Title"
import Table from "./Base/Table" import Table from "./Base/Table"
import { import {RenderingSpecification, SpecialVisualization, SpecialVisualizationState,} from "./SpecialVisualization"
RenderingSpecification, import {HistogramViz} from "./Popup/HistogramViz"
SpecialVisualization, import {MinimapViz} from "./Popup/MinimapViz"
SpecialVisualizationState, import {ShareLinkViz} from "./Popup/ShareLinkViz"
} from "./SpecialVisualization" import {UploadToOsmViz} from "./Popup/UploadToOsmViz"
import { HistogramViz } from "./Popup/HistogramViz" import {MultiApplyViz} from "./Popup/MultiApplyViz"
import { MinimapViz } from "./Popup/MinimapViz" import {AddNoteCommentViz} from "./Popup/AddNoteCommentViz"
import { ShareLinkViz } from "./Popup/ShareLinkViz" import {PlantNetDetectionViz} from "./Popup/PlantNetDetectionViz"
import { UploadToOsmViz } from "./Popup/UploadToOsmViz"
import { MultiApplyViz } from "./Popup/MultiApplyViz"
import { AddNoteCommentViz } from "./Popup/AddNoteCommentViz"
import { PlantNetDetectionViz } from "./Popup/PlantNetDetectionViz"
import TagApplyButton from "./Popup/TagApplyButton" import TagApplyButton from "./Popup/TagApplyButton"
import { CloseNoteButton } from "./Popup/CloseNoteButton" import {CloseNoteButton} from "./Popup/CloseNoteButton"
import { MapillaryLinkVis } from "./Popup/MapillaryLinkVis" import {MapillaryLinkVis} from "./Popup/MapillaryLinkVis"
import { Store, Stores, UIEventSource } from "../Logic/UIEventSource" import {Store, Stores, UIEventSource} from "../Logic/UIEventSource"
import AllTagsPanel from "./Popup/AllTagsPanel.svelte" import AllTagsPanel from "./Popup/AllTagsPanel.svelte"
import AllImageProviders from "../Logic/ImageProviders/AllImageProviders" import AllImageProviders from "../Logic/ImageProviders/AllImageProviders"
import { ImageCarousel } from "./Image/ImageCarousel" import {ImageCarousel} from "./Image/ImageCarousel"
import { ImageUploadFlow } from "./Image/ImageUploadFlow" import {ImageUploadFlow} from "./Image/ImageUploadFlow"
import { VariableUiElement } from "./Base/VariableUIElement" import {VariableUiElement} from "./Base/VariableUIElement"
import { Utils } from "../Utils" import {Utils} from "../Utils"
import Wikidata, { WikidataResponse } from "../Logic/Web/Wikidata" import Wikidata, {WikidataResponse} from "../Logic/Web/Wikidata"
import { Translation } from "./i18n/Translation" import {Translation} from "./i18n/Translation"
import Translations from "./i18n/Translations" import Translations from "./i18n/Translations"
import ReviewForm from "./Reviews/ReviewForm" import ReviewForm from "./Reviews/ReviewForm"
import ReviewElement from "./Reviews/ReviewElement" import ReviewElement from "./Reviews/ReviewElement"
import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization" import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization"
import LiveQueryHandler from "../Logic/Web/LiveQueryHandler" import LiveQueryHandler from "../Logic/Web/LiveQueryHandler"
import { SubtleButton } from "./Base/SubtleButton" import {SubtleButton} from "./Base/SubtleButton"
import Svg from "../Svg" import Svg from "../Svg"
import NoteCommentElement from "./Popup/NoteCommentElement" import NoteCommentElement from "./Popup/NoteCommentElement"
import ImgurUploader from "../Logic/ImageProviders/ImgurUploader" import ImgurUploader from "../Logic/ImageProviders/ImgurUploader"
import FileSelectorButton from "./Input/FileSelectorButton" import FileSelectorButton from "./Input/FileSelectorButton"
import { LoginToggle } from "./Popup/LoginButton" import {LoginToggle} from "./Popup/LoginButton"
import Toggle from "./Input/Toggle" import Toggle from "./Input/Toggle"
import { SubstitutedTranslation } from "./SubstitutedTranslation" import {SubstitutedTranslation} from "./SubstitutedTranslation"
import List from "./Base/List" import List from "./Base/List"
import StatisticsPanel from "./BigComponents/StatisticsPanel" import StatisticsPanel from "./BigComponents/StatisticsPanel"
import AutoApplyButton from "./Popup/AutoApplyButton" import AutoApplyButton from "./Popup/AutoApplyButton"
import { LanguageElement } from "./Popup/LanguageElement" import {LanguageElement} from "./Popup/LanguageElement"
import FeatureReviews from "../Logic/Web/MangroveReviews" import FeatureReviews from "../Logic/Web/MangroveReviews"
import Maproulette from "../Logic/Maproulette" import Maproulette from "../Logic/Maproulette"
import SvelteUIElement from "./Base/SvelteUIElement" import SvelteUIElement from "./Base/SvelteUIElement"
import { BBoxFeatureSourceForLayer } from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource" import {BBoxFeatureSourceForLayer} from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource"
import QuestionViz from "./Popup/QuestionViz" import QuestionViz from "./Popup/QuestionViz"
import { Feature, Point } from "geojson" import {Feature, Point} from "geojson"
import { GeoOperations } from "../Logic/GeoOperations" import {GeoOperations} from "../Logic/GeoOperations"
import CreateNewNote from "./Popup/CreateNewNote.svelte" import CreateNewNote from "./Popup/CreateNewNote.svelte"
import AddNewPoint from "./Popup/AddNewPoint/AddNewPoint.svelte" import AddNewPoint from "./Popup/AddNewPoint/AddNewPoint.svelte"
import UserProfile from "./BigComponents/UserProfile.svelte" import UserProfile from "./BigComponents/UserProfile.svelte"
@ -58,30 +54,27 @@ import LanguagePicker from "./LanguagePicker"
import Link from "./Base/Link" import Link from "./Base/Link"
import LayerConfig from "../Models/ThemeConfig/LayerConfig" import LayerConfig from "../Models/ThemeConfig/LayerConfig"
import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig" import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"
import NearbyImages, { import NearbyImages, {NearbyImageOptions, P4CPicture, SelectOneNearbyImage,} from "./Popup/NearbyImages"
NearbyImageOptions, import {Tag} from "../Logic/Tags/Tag"
P4CPicture,
SelectOneNearbyImage,
} from "./Popup/NearbyImages"
import { Tag } from "../Logic/Tags/Tag"
import ChangeTagAction from "../Logic/Osm/Actions/ChangeTagAction" import ChangeTagAction from "../Logic/Osm/Actions/ChangeTagAction"
import { And } from "../Logic/Tags/And" import {And} from "../Logic/Tags/And"
import { SaveButton } from "./Popup/SaveButton" import {SaveButton} from "./Popup/SaveButton"
import Lazy from "./Base/Lazy" import Lazy from "./Base/Lazy"
import { CheckBox } from "./Input/Checkboxes" import {CheckBox} from "./Input/Checkboxes"
import Slider from "./Input/Slider" import Slider from "./Input/Slider"
import { OsmTags, WayId } from "../Models/OsmFeature" import {OsmTags, WayId} from "../Models/OsmFeature"
import MoveWizard from "./Popup/MoveWizard" import MoveWizard from "./Popup/MoveWizard"
import SplitRoadWizard from "./Popup/SplitRoadWizard" import SplitRoadWizard from "./Popup/SplitRoadWizard"
import { ExportAsGpxViz } from "./Popup/ExportAsGpxViz" import {ExportAsGpxViz} from "./Popup/ExportAsGpxViz"
import WikipediaPanel from "./Wikipedia/WikipediaPanel.svelte" import WikipediaPanel from "./Wikipedia/WikipediaPanel.svelte"
import TagRenderingEditable from "./Popup/TagRendering/TagRenderingEditable.svelte" import TagRenderingEditable from "./Popup/TagRendering/TagRenderingEditable.svelte"
import { PointImportButtonViz } from "./Popup/ImportButtons/PointImportButtonViz" import {PointImportButtonViz} from "./Popup/ImportButtons/PointImportButtonViz"
import WayImportButtonViz from "./Popup/ImportButtons/WayImportButtonViz" import WayImportButtonViz from "./Popup/ImportButtons/WayImportButtonViz"
import ConflateImportButtonViz from "./Popup/ImportButtons/ConflateImportButtonViz" import ConflateImportButtonViz from "./Popup/ImportButtons/ConflateImportButtonViz"
import DeleteWizard from "./Popup/DeleteFlow/DeleteWizard.svelte" import DeleteWizard from "./Popup/DeleteFlow/DeleteWizard.svelte"
import { OpenJosm } from "./BigComponents/OpenJosm" import {OpenJosm} from "./BigComponents/OpenJosm"
import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte" import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte"
import FediverseValidator from "./InputElement/Validators/FediverseValidator";
class NearbyImageVis implements SpecialVisualization { class NearbyImageVis implements SpecialVisualization {
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests // Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
@ -180,7 +173,7 @@ class NearbyImageVis implements SpecialVisualization {
towardsCenter, towardsCenter,
new Combine([ new Combine([
new VariableUiElement( new VariableUiElement(
radius.GetValue().map((radius) => t.withinRadius.Subs({ radius })) radius.GetValue().map((radius) => t.withinRadius.Subs({radius}))
), ),
radius, radius,
]).SetClass("flex justify-between"), ]).SetClass("flex justify-between"),
@ -393,24 +386,24 @@ export default class SpecialVisualizations {
viz.docs, viz.docs,
viz.args.length > 0 viz.args.length > 0
? new Table( ? new Table(
["name", "default", "description"], ["name", "default", "description"],
viz.args.map((arg) => { viz.args.map((arg) => {
let defaultArg = arg.defaultValue ?? "_undefined_" let defaultArg = arg.defaultValue ?? "_undefined_"
if (defaultArg == "") { if (defaultArg == "") {
defaultArg = "_empty string_" defaultArg = "_empty string_"
} }
return [arg.name, defaultArg, arg.doc] return [arg.name, defaultArg, arg.doc]
}) })
) )
: undefined, : undefined,
new Title("Example usage of " + viz.funcName, 4), new Title("Example usage of " + viz.funcName, 4),
new FixedUiElement( new FixedUiElement(
viz.example ?? viz.example ??
"`{" + "`{" +
viz.funcName + viz.funcName +
"(" + "(" +
viz.args.map((arg) => arg.defaultValue).join(",") + viz.args.map((arg) => arg.defaultValue).join(",") +
")}`" ")}`"
).SetClass("literal-code"), ).SetClass("literal-code"),
]) ])
} }
@ -469,14 +462,14 @@ export default class SpecialVisualizations {
s.structuredExamples === undefined s.structuredExamples === undefined
? [] ? []
: s.structuredExamples().map((e) => { : s.structuredExamples().map((e) => {
return s.constr( return s.constr(
state, state,
new UIEventSource<Record<string, string>>(e.feature.properties), new UIEventSource<Record<string, string>>(e.feature.properties),
e.args, e.args,
e.feature, e.feature,
undefined undefined
) )
}) })
return new Combine([new Title(s.funcName), s.docs, ...examples]) return new Combine([new Title(s.funcName), s.docs, ...examples])
} }
@ -491,7 +484,7 @@ export default class SpecialVisualizations {
let [lon, lat] = GeoOperations.centerpointCoordinates(feature) let [lon, lat] = GeoOperations.centerpointCoordinates(feature)
return new SvelteUIElement(AddNewPoint, { return new SvelteUIElement(AddNewPoint, {
state, state,
coordinate: { lon, lat }, coordinate: {lon, lat},
}) })
}, },
}, },
@ -610,7 +603,7 @@ export default class SpecialVisualizations {
feature: Feature feature: Feature
): BaseUIElement { ): BaseUIElement {
const [lon, lat] = GeoOperations.centerpointCoordinates(feature) const [lon, lat] = GeoOperations.centerpointCoordinates(feature)
return new SvelteUIElement(CreateNewNote, { state, coordinate: { lon, lat } }) return new SvelteUIElement(CreateNewNote, {state, coordinate: {lon, lat}})
}, },
}, },
new CloseNoteButton(), new CloseNoteButton(),
@ -687,7 +680,7 @@ export default class SpecialVisualizations {
docs: "Prints all key-value pairs of the object - used for debugging", docs: "Prints all key-value pairs of the object - used for debugging",
args: [], args: [],
constr: (state, tags: UIEventSource<any>) => constr: (state, tags: UIEventSource<any>) =>
new SvelteUIElement(AllTagsPanel, { tags, state }), new SvelteUIElement(AllTagsPanel, {tags, state}),
}, },
{ {
funcName: "image_carousel", funcName: "image_carousel",
@ -1326,7 +1319,7 @@ export default class SpecialVisualizations {
], ],
constr(state, featureTags, args) { constr(state, featureTags, args) {
const [key, tr] = args const [key, tr] = args
const translation = new Translation({ "*": tr }) const translation = new Translation({"*": tr})
return new VariableUiElement( return new VariableUiElement(
featureTags.map((tags) => { featureTags.map((tags) => {
const properties: object[] = JSON.parse(tags[key]) const properties: object[] = JSON.parse(tags[key])
@ -1344,12 +1337,32 @@ export default class SpecialVisualizations {
) )
}, },
}, },
{
funcName: "fediverse_link",
docs: "Converts a fediverse username or link into a clickable link",
args: [{
name: "key",
doc: "The attribute-name containing the link",
required: true
}],
constr(state: SpecialVisualizationState, tagSource: UIEventSource<Record<string, string>>, argument: string[], feature: Feature, layer: LayerConfig): BaseUIElement {
const key = argument[0]
const validator = new FediverseValidator()
return new VariableUiElement(tagSource.map(tags => tags[key]).map(fediAccount => {
fediAccount = validator.reformat(fediAccount)
const [_, username, host] = fediAccount.match(FediverseValidator.usernameAtServer)
return new Link(fediAccount, "https://" + host + "/@" + username, true)
}
))
}
}
] ]
specialVisualizations.push(new AutoApplyButton(specialVisualizations)) specialVisualizations.push(new AutoApplyButton(specialVisualizations))
const invalid = specialVisualizations const invalid = specialVisualizations
.map((sp, i) => ({ sp, i })) .map((sp, i) => ({sp, i}))
.filter((sp) => sp.sp.funcName === undefined) .filter((sp) => sp.sp.funcName === undefined)
if (invalid.length > 0) { if (invalid.length > 0) {
throw ( throw (