Merge develop

This commit is contained in:
Pieter Vander Vennet 2023-09-19 14:19:58 +02:00
commit 4cb52e37cd
163 changed files with 7245 additions and 4677 deletions

View file

@ -34,6 +34,7 @@
class={twMerge(options.extraClasses, "button text-ellipsis")}
{href}
target={newTab ? "_blank" : undefined}
rel={newTab ? "noopener" : undefined}
>
<slot name="image">
{#if imageUrl !== undefined}

View file

@ -29,7 +29,7 @@ export default class Table extends BaseUIElement {
const header = Utils.NoNull(headerMarkdownParts).join(" | ")
const headerSep = headerMarkdownParts.map((part) => "-".repeat(part.length + 2)).join(" | ")
const table = this._contents
.map((row) => row.map((el) => el?.AsMarkdown()?.replace("|", "\\|") ?? " ").join(" | "))
.map((row) => row.map((el) => el?.AsMarkdown()?.replaceAll("\\","\\\\")?.replaceAll("|", "\\|") ?? " ").join(" | "))
.join("\n")
return "\n\n" + [header, headerSep, table, ""].join("\n")

View file

@ -35,7 +35,7 @@
src={`https://raw.githubusercontent.com/pietervdvn/MapComplete-data/main/community_index/${resource.type}.svg`}
/>
<div class="flex flex-col">
<a href={resource.resolved.url} target="_blank" rel="noreferrer nofollow" class="font-bold">
<a href={resource.resolved.url} target="_blank" rel="noreferrer nofollow noopener" class="font-bold">
{resource.resolved.name ?? resource.resolved.url}
</a>
{resource.resolved?.description}

View file

@ -102,7 +102,7 @@ export default class CopyrightPanel extends Combine {
let bgAttr: BaseUIElement | string = undefined
if (attrText && attrUrl) {
bgAttr =
"<a href='" + attrUrl + "' target='_blank'>" + attrText + "</a>"
"<a href='" + attrUrl + "' target='_blank' rel='noopener'>" + attrText + "</a>"
} else if (attrUrl) {
bgAttr = attrUrl
} else {

View file

@ -16,7 +16,7 @@ export class OpenJosm extends Combine {
const josmState = new UIEventSource<string>(undefined)
// Reset after 15s
josmState.stabilized(15000).addCallbackD((_) => josmState.setData(undefined))
josmState.stabilized(15000).addCallbackD(() => josmState.setData(undefined))
const stateIndication = new VariableUiElement(
josmState.map((state) => {
@ -45,7 +45,7 @@ export class OpenJosm extends Combine {
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"))
.catch(() => josmState.setData("ERROR"))
})
.SetClass("w-full"),
undefined,

View file

@ -37,6 +37,7 @@
<a
href={osmConnection.Backend() + "/profile/edit"}
target="_blank"
rel="noopener"
class="link-no-underline flex items-center self-end"
>
<PencilAltIcon slot="image" class="h-8 w-8 p-2" />

View file

@ -81,7 +81,7 @@
mimetype="image/png"
mainText={t.downloadAsPng}
helperText={t.downloadAsPngHelper}
construct={(_) => state.mapProperties.exportAsPng(4)}
construct={() => state.mapProperties.exportAsPng(4)}
/>
<div class="flex flex-col">

View file

@ -73,7 +73,7 @@ export class ImageUploadFlow extends Toggle {
]).SetClass("w-full flex justify-center items-center")
const licenseStore = state?.osmConnection?.GetPreference(
Constants.OsmPreferenceKeyPicturesLicense,
"pictures-license",
"CC0"
)

View file

@ -172,7 +172,8 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
tileSize: layer["tile-size"] ?? 256,
minzoom: layer["min_zoom"] ?? 1,
maxzoom: layer["max_zoom"] ?? 25,
// scheme: background["type"] === "tms" ? "tms" : "xyz",
// Bit of a hack, but seems to work
scheme: layer.url.includes("{-y}") ? "tms" : "xyz",
}
}
@ -203,6 +204,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
"{width}": "" + size,
"{height}": "" + size,
"{zoom}": "{z}",
"{-y}": "{y}",
}
for (const key in toReplace) {

View file

@ -0,0 +1,73 @@
<script lang="ts">
import { Store } from "../../Logic/UIEventSource";
import type { OsmTags } from "../../Models/OsmFeature";
import type { SpecialVisualizationState } from "../SpecialVisualization";
import type { P4CPicture } from "../../Logic/Web/NearbyImagesSearch";
import ToSvelte from "../Base/ToSvelte.svelte";
import { AttributedImage } from "../Image/AttributedImage";
import AllImageProviders from "../../Logic/ImageProviders/AllImageProviders";
import LinkPicture from "../../Logic/Osm/Actions/LinkPicture";
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction";
import { Tag } from "../../Logic/Tags/Tag";
import { GeoOperations } from "../../Logic/GeoOperations";
import type { Feature } from "geojson";
import Translations from "../i18n/Translations";
import SpecialTranslation from "./TagRendering/SpecialTranslation.svelte";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
export let tags: Store<OsmTags>;
export let lon: number;
export let lat: number;
export let state: SpecialVisualizationState;
export let image: P4CPicture;
export let feature: Feature;
export let layer: LayerConfig;
export let linkable = true;
let isLinked = false;
const t = Translations.t.image.nearby;
const c = [lon, lat];
let attributedImage = new AttributedImage({
url: image.thumbUrl ?? image.pictureUrl,
provider: AllImageProviders.byName(image.provider),
date: new Date(image.date)
});
let distance = Math.round(GeoOperations.distanceBetween([image.coordinates.lng, image.coordinates.lat], c));
$: {
const currentTags = tags.data;
const key = Object.keys(image.osmTags)[0];
const url = image.osmTags[key];
if (isLinked) {
const action = new LinkPicture(
currentTags.id,
key,
url,
currentTags,
{
theme: state.layout.id,
changeType: "link-image"
}
);
state.changes.applyAction(action);
} else {
for (const k in currentTags) {
const v = currentTags[k];
if (v === url) {
const action = new ChangeTagAction(currentTags.id, new Tag(k, ""), currentTags, { theme: state.layout.id, changeType: "remove-image" });
state.changes.applyAction(action);
}
}
}
}
</script>
<div class="flex flex-col w-fit shrink-0">
<ToSvelte construct={attributedImage.SetClass("h-48 w-fit")} />
{#if linkable}
<label>
<input bind:checked={isLinked} type="checkbox">
<SpecialTranslation t={t.link} {tags} {state} {layer} {feature} />
</label>
{/if}
</div>

View file

@ -0,0 +1,54 @@
<script lang="ts">/**
* Show nearby images which can be clicked
*/
import type { OsmTags } from "../../Models/OsmFeature";
import { Store, UIEventSource } from "../../Logic/UIEventSource";
import type { SpecialVisualizationState } from "../SpecialVisualization";
import type { P4CPicture } from "../../Logic/Web/NearbyImagesSearch";
import NearbyImagesSearch from "../../Logic/Web/NearbyImagesSearch";
import LinkableImage from "./LinkableImage.svelte";
import type { Feature } from "geojson";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import Loading from "../Base/Loading.svelte";
import AllImageProviders from "../../Logic/ImageProviders/AllImageProviders";
import Tr from "../Base/Tr.svelte";
import Translations from "../i18n/Translations";
export let tags: Store<OsmTags>;
export let state: SpecialVisualizationState;
export let lon: number;
export let lat: number;
export let feature: Feature;
export let linkable: boolean = true;
export let layer: LayerConfig;
let imagesProvider = new NearbyImagesSearch({
lon, lat, allowSpherical: new UIEventSource<boolean>(false),
blacklist: AllImageProviders.LoadImagesFor(tags)
}, state.indexedFeatures);
let images: Store<P4CPicture[]> = imagesProvider.store.map(images => images.slice(0, 20));
</script>
<div class="interactive rounded-2xl border-interactive p-2">
<div class="flex justify-between">
<h4>
<Tr t={Translations.t.image.nearby.title} />
</h4>
<slot name="corner" />
</div>
{#if $images.length === 0}
<Loading />
{:else}
<div class="overflow-x-auto w-full flex space-x-1" style="scroll-snap-type: x proximity">
{#each $images as image (image.pictureUrl)}
<span class="w-fit shrink-0" style="scroll-snap-align: start">
<LinkableImage {tags} {image} {state} {lon} {lat} {feature} {layer} {linkable} />
</span>
{/each}
</div>
{/if}
</div>

View file

@ -1,316 +0,0 @@
import Combine from "../Base/Combine"
import { Store, Stores, UIEventSource } from "../../Logic/UIEventSource"
import { SlideShow } from "../Image/SlideShow"
import { ClickableToggle } from "../Input/Toggle"
import Loading from "../Base/Loading"
import { AttributedImage } from "../Image/AttributedImage"
import AllImageProviders from "../../Logic/ImageProviders/AllImageProviders"
import Svg from "../../Svg"
import BaseUIElement from "../BaseUIElement"
import { InputElement } from "../Input/InputElement"
import { VariableUiElement } from "../Base/VariableUIElement"
import Translations from "../i18n/Translations"
import { Mapillary } from "../../Logic/ImageProviders/Mapillary"
import { SubtleButton } from "../Base/SubtleButton"
import { GeoOperations } from "../../Logic/GeoOperations"
import Lazy from "../Base/Lazy"
import P4C from "pic4carto"
import { IndexedFeatureSource } from "../../Logic/FeatureSource/FeatureSource"
export interface P4CPicture {
pictureUrl: string
date?: number
coordinates: { lat: number; lng: number }
provider: "Mapillary" | string
author?
license?
detailsUrl?: string
direction?
osmTags?: object /*To copy straight into OSM!*/
thumbUrl: string
details: {
isSpherical: boolean
}
}
export interface NearbyImageOptions {
lon: number
lat: number
// Radius of the upstream search
searchRadius?: 500 | number
maxDaysOld?: 1095 | number
blacklist: Store<{ url: string }[]>
shownImagesCount?: UIEventSource<number>
towardscenter?: UIEventSource<boolean>
allowSpherical?: UIEventSource<boolean>
// Radius of what is shown. Useless to select a value > searchRadius; defaults to searchRadius
shownRadius?: UIEventSource<number>
}
class ImagesInLoadedDataFetcher {
private indexedFeatures: IndexedFeatureSource
constructor(indexedFeatures: IndexedFeatureSource) {
this.indexedFeatures = indexedFeatures
}
public fetchAround(loc: { lon: number; lat: number; searchRadius?: number }): P4CPicture[] {
const foundImages: P4CPicture[] = []
this.indexedFeatures.features.data.forEach((feature) => {
const props = feature.properties
const images = []
if (props.image) {
images.push(props.image)
}
for (let i = 0; i < 10; i++) {
if (props["image:" + i]) {
images.push(props["image:" + i])
}
}
if (images.length == 0) {
return
}
const centerpoint = GeoOperations.centerpointCoordinates(feature)
const d = GeoOperations.distanceBetween(centerpoint, [loc.lon, loc.lat])
if (loc.searchRadius !== undefined && d > loc.searchRadius) {
return
}
for (const image of images) {
foundImages.push({
pictureUrl: image,
thumbUrl: image,
coordinates: { lng: centerpoint[0], lat: centerpoint[1] },
provider: "OpenStreetMap",
details: {
isSpherical: false,
},
})
}
})
const cleaned: P4CPicture[] = []
const seen = new Set<string>()
for (const foundImage of foundImages) {
if (seen.has(foundImage.pictureUrl)) {
continue
}
seen.add(foundImage.pictureUrl)
cleaned.push(foundImage)
}
return cleaned
}
}
export default class NearbyImages extends Lazy {
constructor(options: NearbyImageOptions, state?: IndexedFeatureSource) {
super(() => {
const t = Translations.t.image.nearbyPictures
const shownImages = options.shownImagesCount ?? new UIEventSource(25)
const loadedPictures = NearbyImages.buildPictureFetcher(options, state)
const loadMoreButton = new Combine([
new SubtleButton(Svg.add_svg(), t.loadMore).onClick(() => {
shownImages.setData(shownImages.data + 25)
}),
]).SetClass("flex flex-col justify-center")
const imageElements = loadedPictures.map(
(imgs) => {
if (imgs === undefined) {
return []
}
const elements = (imgs.images ?? [])
.slice(0, shownImages.data)
.map((i) => this.prepareElement(i))
if (imgs.images !== undefined && elements.length < imgs.images.length) {
// We effectively sliced some items, so we can increase the count
elements.push(loadMoreButton)
}
return elements
},
[shownImages]
)
return new VariableUiElement(
loadedPictures.map((loaded) => {
if (loaded?.images === undefined) {
return NearbyImages.NoImagesView(new Loading(t.loading)).SetClass(
"animate-pulse"
)
}
const images = loaded.images
const beforeFilter = loaded?.beforeFilter
if (beforeFilter === 0) {
return NearbyImages.NoImagesView(t.nothingFound.SetClass("alert block"))
} else if (images.length === 0) {
const removeFiltersButton = new SubtleButton(
Svg.filter_disable_svg(),
t.removeFilters
).onClick(() => {
options.shownRadius.setData(options.searchRadius)
options.allowSpherical.setData(true)
options.towardscenter.setData(false)
})
return NearbyImages.NoImagesView(
t.allFiltered.SetClass("font-bold"),
removeFiltersButton
)
}
return new SlideShow(imageElements)
})
)
})
}
private static NoImagesView(...elems: BaseUIElement[]) {
return new Combine(elems)
.SetClass("flex flex-col justify-center items-center bg-gray-200 mb-2 rounded-lg")
.SetStyle(
"height: calc( var(--image-carousel-height) - 0.5rem ) ; max-height: calc( var(--image-carousel-height) - 0.5rem );"
)
}
private static buildPictureFetcher(options: NearbyImageOptions, state?: IndexedFeatureSource) {
const picManager = new P4C.PicturesManager({})
const searchRadius = options.searchRadius ?? 500
const nearbyImages =
state !== undefined ? new ImagesInLoadedDataFetcher(state).fetchAround(options) : []
return Stores.FromPromise<P4CPicture[]>(
picManager.startPicsRetrievalAround(
new P4C.LatLng(options.lat, options.lon),
options.searchRadius ?? 500,
{
mindate:
new Date().getTime() -
(options.maxDaysOld ?? 3 * 365) * 24 * 60 * 60 * 1000,
towardscenter: false,
}
)
).map(
(images) => {
if (images === undefined) {
return undefined
}
images = (images ?? []).concat(nearbyImages)
const blacklisted = options.blacklist?.data
images = images?.filter(
(i) =>
!blacklisted?.some((notAllowed) =>
Mapillary.sameUrl(i.pictureUrl, notAllowed.url)
)
)
const beforeFilterCount = images.length
if (!options?.allowSpherical?.data) {
images = images?.filter((i) => i.details.isSpherical !== true)
}
const shownRadius = options?.shownRadius?.data ?? searchRadius
if (shownRadius !== searchRadius) {
images = images.filter((i) => {
const d = GeoOperations.distanceBetween(
[i.coordinates.lng, i.coordinates.lat],
[options.lon, options.lat]
)
return d <= shownRadius
})
}
if (options.towardscenter?.data) {
images = images.filter((i) => {
if (i.direction === undefined || isNaN(i.direction)) {
return false
}
const bearing = GeoOperations.bearing(
[i.coordinates.lng, i.coordinates.lat],
[options.lon, options.lat]
)
const diff = Math.abs((i.direction - bearing) % 360)
return diff < 40
})
}
images?.sort((a, b) => {
const distanceA = GeoOperations.distanceBetween(
[a.coordinates.lng, a.coordinates.lat],
[options.lon, options.lat]
)
const distanceB = GeoOperations.distanceBetween(
[b.coordinates.lng, b.coordinates.lat],
[options.lon, options.lat]
)
return distanceA - distanceB
})
return { images, beforeFilter: beforeFilterCount }
},
[options.blacklist, options.allowSpherical, options.towardscenter, options.shownRadius]
)
}
protected prepareElement(info: P4CPicture): BaseUIElement {
const provider = AllImageProviders.byName(info.provider)
return new AttributedImage({ url: info.pictureUrl, provider })
}
private static asAttributedImage(info: P4CPicture): AttributedImage {
const provider = AllImageProviders.byName(info.provider)
return new AttributedImage({ url: info.thumbUrl, provider, date: new Date(info.date) })
}
protected asToggle(info: P4CPicture): ClickableToggle {
const imgNonSelected = NearbyImages.asAttributedImage(info)
const imageSelected = NearbyImages.asAttributedImage(info)
const nonSelected = new Combine([imgNonSelected]).SetClass("relative block")
const hoveringCheckmark = new Combine([
Svg.confirm_svg().SetClass("block w-24 h-24 -ml-12 -mt-12"),
]).SetClass("absolute left-1/2 top-1/2 w-0")
const selected = new Combine([imageSelected, hoveringCheckmark]).SetClass("relative block")
return new ClickableToggle(selected, nonSelected).SetClass("").ToggleOnClick()
}
}
export class SelectOneNearbyImage extends NearbyImages implements InputElement<P4CPicture> {
private readonly value: UIEventSource<P4CPicture>
constructor(
options: NearbyImageOptions & { value?: UIEventSource<P4CPicture> },
state?: IndexedFeatureSource
) {
super(options, state)
this.value = options.value ?? new UIEventSource<P4CPicture>(undefined)
}
GetValue(): UIEventSource<P4CPicture> {
return this.value
}
IsValid(t: P4CPicture): boolean {
return false
}
protected prepareElement(info: P4CPicture): BaseUIElement {
const toggle = super.asToggle(info)
toggle.isEnabled.addCallback((enabled) => {
if (enabled) {
this.value.setData(info)
} else if (this.value.data === info) {
this.value.setData(undefined)
}
})
this.value.addCallback((inf) => {
if (inf !== info) {
toggle.isEnabled.setData(false)
}
})
return toggle
}
}

View file

@ -0,0 +1,36 @@
<script lang="ts">
import { Store } from "../../Logic/UIEventSource";
import type { OsmTags } from "../../Models/OsmFeature";
import type { SpecialVisualizationState } from "../SpecialVisualization";
import type { Feature } from "geojson";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import Translations from "../i18n/Translations";
import Tr from "../Base/Tr.svelte";
import NearbyImages from "./NearbyImages.svelte";
import Svg from "../../Svg";
import ToSvelte from "../Base/ToSvelte.svelte";
import { XCircleIcon } from "@babeard/svelte-heroicons/solid";
import exp from "constants";
export let tags: Store<OsmTags>;
export let state: SpecialVisualizationState;
export let lon: number;
export let lat: number;
export let feature: Feature;
export let linkable: boolean = true;
export let layer: LayerConfig;
const t = Translations.t.image.nearby;
let expanded = false;
</script>
{#if expanded}
<NearbyImages {tags} {state} {lon} {lat} {feature} {linkable}>
<XCircleIcon slot="corner" class="w-6 h-6 cursor-pointer" on:click={() => {expanded = false}}/>
</NearbyImages>
{:else}
<button class="w-full flex items-center" on:click={() => { expanded = true; }}>
<ToSvelte construct={ Svg.camera_plus_svg().SetClass("block w-8 h-8 p-1 mr-2 ")}/>
<Tr t={t.seeNearby}/></button>
{/if}

View file

@ -0,0 +1,22 @@
<script lang="ts">
import type { OsmTags } from "../../Models/OsmFeature";
import Svg from "../../Svg";
import ToSvelte from "../Base/ToSvelte.svelte";
import { Utils } from "../../Utils";
export let tags: Store<OsmTags>
export let args: string[]
let [to, subject, body, button_text] = args.map(a => Utils.SubstituteKeys(a, $tags))
let url = "mailto:" +
to +
"?subject=" +
encodeURIComponent(subject) +
"&body=" +
encodeURIComponent(body)
</script>
<a class="button flex items-center w-full" href={url}>
<ToSvelte construct={Svg.envelope_svg().SetClass("w-8 h-8 mr-4 shrink-0")}/>
{button_text}
</a>

View file

@ -58,18 +58,6 @@ import LanguagePicker from "./LanguagePicker"
import Link from "./Base/Link"
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"
import NearbyImages, {
NearbyImageOptions,
P4CPicture,
SelectOneNearbyImage,
} from "./Popup/NearbyImages"
import { Tag } from "../Logic/Tags/Tag"
import ChangeTagAction from "../Logic/Osm/Actions/ChangeTagAction"
import { And } from "../Logic/Tags/And"
import { SaveButton } from "./Popup/SaveButton"
import Lazy from "./Base/Lazy"
import { CheckBox } from "./Input/Checkboxes"
import Slider from "./Input/Slider"
import { OsmTags, WayId } from "../Models/OsmFeature"
import MoveWizard from "./Popup/MoveWizard"
import SplitRoadWizard from "./Popup/SplitRoadWizard"
@ -83,19 +71,17 @@ import DeleteWizard from "./Popup/DeleteFlow/DeleteWizard.svelte"
import { OpenJosm } from "./BigComponents/OpenJosm"
import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte"
import FediverseValidator from "./InputElement/Validators/FediverseValidator"
import SendEmail from "./Popup/SendEmail.svelte"
import NearbyImages from "./Popup/NearbyImages.svelte"
import NearbyImagesCollapsed from "./Popup/NearbyImagesCollapsed.svelte"
class NearbyImageVis implements SpecialVisualization {
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
args: { name: string; defaultValue?: string; doc: string; required?: boolean }[] = [
{
name: "mode",
defaultValue: "expandable",
doc: "Indicates how this component is initialized. Options are: \n\n- `open`: always show and load the pictures\n- `collapsable`: show the pictures, but a user can collapse them\n- `expandable`: shown by default; but a user can collapse them.",
},
{
name: "mapillary",
defaultValue: "true",
doc: "If 'true', includes a link to mapillary on this location.",
defaultValue: "closed",
doc: "Either `open` or `closed`. If `open`, then the image carousel will always be shown",
},
]
docs =
@ -104,110 +90,21 @@ class NearbyImageVis implements SpecialVisualization {
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
tags: UIEventSource<Record<string, string>>,
args: string[],
feature: Feature,
layer: LayerConfig
): BaseUIElement {
const t = Translations.t.image.nearbyPictures
const mode: "open" | "expandable" | "collapsable" = <any>args[0]
const isOpen = args[0] === "open"
const [lon, lat] = GeoOperations.centerpointCoordinates(feature)
const id: string = tagSource.data["id"]
const canBeEdited: boolean = !!id?.match("(node|way|relation)/-?[0-9]+")
const selectedImage = new UIEventSource<P4CPicture>(undefined)
let saveButton: BaseUIElement = undefined
if (canBeEdited) {
const confirmText: BaseUIElement = new SubstitutedTranslation(
t.confirm,
tagSource,
state
)
const onSave = async () => {
console.log("Selected a picture...", selectedImage.data)
const osmTags = selectedImage.data.osmTags
const tags: Tag[] = []
for (const key in osmTags) {
tags.push(new Tag(key, osmTags[key]))
}
await state?.changes?.applyAction(
new ChangeTagAction(id, new And(tags), tagSource.data, {
theme: state?.layout.id,
changeType: "link-image",
})
)
}
saveButton = new SaveButton(selectedImage, state, confirmText, t.noImageSelected)
.onClick(onSave)
.SetClass("flex justify-end")
}
const nearby = new Lazy(() => {
const towardsCenter = new CheckBox(t.onlyTowards, false)
const maxSearchRadius = 100
const stepSize = 10
const defaultValue = Math.floor(maxSearchRadius / (2 * stepSize)) * stepSize
const fromOsmPreferences = state?.osmConnection
?.GetPreference("nearby-images-radius", "" + defaultValue)
.sync(
(s) => Number(s),
[],
(i) => "" + i
)
const radiusValue = new UIEventSource(fromOsmPreferences.data)
radiusValue.addCallbackAndRunD((v) => fromOsmPreferences.setData(v))
const radius = new Slider(stepSize, maxSearchRadius, {
value: radiusValue,
step: 10,
})
const alreadyInTheImage = AllImageProviders.LoadImagesFor(tagSource)
const options: NearbyImageOptions & { value } = {
lon,
lat,
searchRadius: maxSearchRadius,
shownRadius: radius.GetValue(),
value: selectedImage,
blacklist: alreadyInTheImage,
towardscenter: towardsCenter.GetValue(),
maxDaysOld: 365 * 3,
}
const slideshow = canBeEdited
? new SelectOneNearbyImage(options, state.indexedFeatures)
: new NearbyImages(options, state.indexedFeatures)
const controls = new Combine([
towardsCenter,
new Combine([
new VariableUiElement(
radius.GetValue().map((radius) => t.withinRadius.Subs({ radius }))
),
radius,
]).SetClass("flex justify-between"),
]).SetClass("flex flex-col")
return new Combine([
slideshow,
controls,
saveButton,
new MapillaryLinkVis().constr(state, tagSource, [], feature).SetClass("mt-6"),
])
return new SvelteUIElement(isOpen ? NearbyImages : NearbyImagesCollapsed, {
tags,
state,
lon,
lat,
feature,
layer,
})
let withEdit: BaseUIElement = nearby
if (canBeEdited) {
withEdit = new Combine([t.hasMatchingPicture, nearby]).SetClass("flex flex-col")
}
if (mode === "open") {
return withEdit
}
const toggleState = new UIEventSource<boolean>(mode === "collapsable")
return new Toggle(
new Combine([new Title(t.title), withEdit]),
new Title(t.browseNearby).onClick(() => toggleState.setData(true)),
toggleState
)
}
}
@ -1237,28 +1134,12 @@ export default class SpecialVisualizations {
},
],
constr(__, tags, args) {
return new VariableUiElement(
tags.map((tags) => {
const [to, subject, body, button_text] = args.map((str) =>
Utils.SubstituteKeys(str, tags)
)
const url =
"mailto:" +
to +
"?subject=" +
encodeURIComponent(subject) +
"&body=" +
encodeURIComponent(body)
return new SubtleButton(Svg.envelope_svg(), button_text, {
url,
})
})
)
return new SvelteUIElement(SendEmail, { args, tags })
},
},
{
funcName: "link",
docs: "Construct a link. By using the 'special' visualisation notation, translation should be easier",
docs: "Construct a link. By using the 'special' visualisation notation, translations should be easier",
args: [
{
name: "text",

View file

@ -1,7 +1,7 @@
<script lang="ts">
import Svg from "../Svg"
import Loading from "./Base/Loading.svelte"
import ToSvelte from "./Base/ToSvelte.svelte"
import Svg from "../Svg";
import Loading from "./Base/Loading.svelte";
import ToSvelte from "./Base/ToSvelte.svelte";
</script>
<div>
@ -38,6 +38,13 @@
<ToSvelte construct={Svg.community_svg().SetClass("w-6 h-6")} />
Main action (disabled)
</button>
<button class="small primary">
Small button
</button>
<button class="small primary disabled">
Small, disabled button
</button>
</div>
<div class="flex">
<button>

View file

@ -226,16 +226,27 @@ export class Translation extends BaseUIElement {
return new Translation(this.translations, this.context)
}
FirstSentence() {
/**
* Build a new translation which only contains the first sentence of every language
* A sentence stops at either a dot (`.`) or a HTML-break ('<br/>').
* The dot or linebreak are _not_ returned.
*
* new Translation({"en": "This is a sentence. This is another sentence"}).FirstSentence().textFor("en") // "This is a sentence"
* new Translation({"en": "This is a sentence <br/> This is another sentence"}).FirstSentence().textFor("en") // "This is a sentence"
* new Translation({"en": "This is a sentence <br> This is another sentence"}).FirstSentence().textFor("en") // "This is a sentence"
* new Translation({"en": "This is a sentence with a <b>bold</b> word. This is another sentence"}).FirstSentence().textFor("en") // "This is a sentence with a <b>bold</b> word"
* @constructor
*/
public FirstSentence(): Translation {
const tr = {}
for (const lng in this.translations) {
if (!this.translations.hasOwnProperty(lng)) {
continue
}
let txt = this.translations[lng]
txt = txt.replace(/[.<].*/, "")
txt = txt.replace(/(\.|<br\/>|<br>).*/, "")
txt = Utils.EllipsesAfter(txt, 255)
tr[lng] = txt
tr[lng] = txt.trim()
}
return new Translation(tr)