Styling tweak

Add mapillary link to nearby_images

Fix licenses

Add missing assets

First version of nearby-images
This commit is contained in:
Pieter Vander Vennet 2022-05-06 12:41:24 +02:00
parent a4f2fa63a5
commit 7559f9259b
52 changed files with 674 additions and 207 deletions

View file

@ -24,6 +24,7 @@ import ContributorCount from "../../Logic/ContributorCount";
import Img from "../Base/Img";
import {TypedTranslation} from "../i18n/Translation";
import TranslatorsPanel from "./TranslatorsPanel";
import {MapillaryLink} from "./MapillaryLink";
export class OpenIdEditor extends VariableUiElement {
constructor(state: { locationControl: UIEventSource<Loc> }, iconStyle?: string, objectId?: string) {
@ -44,19 +45,6 @@ export class OpenIdEditor extends VariableUiElement {
}
export class OpenMapillary extends VariableUiElement {
constructor(state: { locationControl: UIEventSource<Loc> }, iconStyle?: string) {
const t = Translations.t.general.attribution
super(state.locationControl.map(location => {
const mapillaryLink = `https://www.mapillary.com/app/?focus=map&lat=${location?.lat ?? 0}&lng=${location?.lon ?? 0}&z=${Math.max((location?.zoom ?? 2) - 1, 1)}`
return new SubtleButton(Svg.mapillary_black_ui().SetStyle(iconStyle), t.openMapillary, {
url: mapillaryLink,
newTab: true
})
}))
}
}
export class OpenJosm extends Combine {
constructor(state: { osmConnection: OsmConnection, currentBounds: UIEventSource<BBox>, }, iconStyle?: string) {
@ -132,7 +120,7 @@ export default class CopyrightPanel extends Combine {
newTab: true
}),
new OpenIdEditor(state, iconStyle),
new OpenMapillary(state, iconStyle),
new MapillaryLink(state, iconStyle),
new OpenJosm(state, iconStyle),
new TranslatorsPanel(state, iconStyle)

View file

@ -0,0 +1,24 @@
import {VariableUiElement} from "../Base/VariableUIElement";
import {UIEventSource} from "../../Logic/UIEventSource";
import Loc from "../../Models/Loc";
import Translations from "../i18n/Translations";
import {SubtleButton} from "../Base/SubtleButton";
import Svg from "../../Svg";
import Combine from "../Base/Combine";
import Title from "../Base/Title";
export class MapillaryLink extends VariableUiElement {
constructor(state: { locationControl: UIEventSource<Loc> }, iconStyle?: string) {
const t = Translations.t.general.attribution
super(state.locationControl.map(location => {
const mapillaryLink = `https://www.mapillary.com/app/?focus=map&lat=${location?.lat ?? 0}&lng=${location?.lon ?? 0}&z=${Math.max((location?.zoom ?? 2) - 1, 1)}`
return new SubtleButton(Svg.mapillary_black_ui().SetStyle(iconStyle),
new Combine([
new Title(t.openMapillary,3),
t.mapillaryHelp]), {
url: mapillaryLink,
newTab: true
}).SetClass("flex flex-col link-no-underline")
}))
}
}

View file

@ -1,22 +1,31 @@
import Combine from "../Base/Combine";
import Attribution from "./Attribution";
import Img from "../Base/Img";
import {ProvidedImage} from "../../Logic/ImageProviders/ImageProvider";
import ImageProvider, {ProvidedImage} from "../../Logic/ImageProviders/ImageProvider";
import BaseUIElement from "../BaseUIElement";
import {Mapillary} from "../../Logic/ImageProviders/Mapillary";
export class AttributedImage extends Combine {
constructor(imageInfo: ProvidedImage) {
constructor(imageInfo: {
url: string,
provider?: ImageProvider,
date?: Date
}
) {
let img: BaseUIElement;
let attr: BaseUIElement
img = new Img(imageInfo.url, false, {
fallbackImage: imageInfo.provider === Mapillary.singleton ? "./assets/svg/blocked.svg" : undefined
});
attr = new Attribution(imageInfo.provider.GetAttributionFor(imageInfo.url),
imageInfo.provider.SourceIcon(),
)
let attr: BaseUIElement = undefined
if(imageInfo.provider !== undefined){
attr = new Attribution(imageInfo.provider?.GetAttributionFor(imageInfo.url),
imageInfo.provider?.SourceIcon(),
imageInfo.date
)
}
super([img, attr]);

View file

@ -4,10 +4,11 @@ import BaseUIElement from "../BaseUIElement";
import {VariableUiElement} from "../Base/VariableUIElement";
import {UIEventSource} from "../../Logic/UIEventSource";
import {LicenseInfo} from "../../Logic/ImageProviders/LicenseInfo";
import {FixedUiElement} from "../Base/FixedUiElement";
export default class Attribution extends VariableUiElement {
constructor(license: UIEventSource<LicenseInfo>, icon: BaseUIElement) {
constructor(license: UIEventSource<LicenseInfo>, icon: BaseUIElement, date?: Date) {
if (license === undefined) {
throw "No license source given in the attribution element"
}
@ -23,7 +24,8 @@ export default class Attribution extends VariableUiElement {
new Combine([
Translations.W(license?.title).SetClass("block"),
Translations.W(license?.artist ?? "").SetClass("block font-bold"),
Translations.W((license?.license ?? "") === "" ? "CC0" : (license?.license ?? ""))
Translations.W((license?.license ?? "") === "" ? "CC0" : (license?.license ?? "")),
date === undefined ? undefined : new FixedUiElement(date.toLocaleDateString())
]).SetClass("flex flex-col")
]).SetClass("flex flex-row bg-black text-white text-sm absolute bottom-0 left-0 p-0.5 pl-5 pr-3 rounded-lg no-images")

133
UI/Popup/NearbyImages.ts Normal file
View file

@ -0,0 +1,133 @@
import Combine from "../Base/Combine";
import {UIEventSource} from "../../Logic/UIEventSource";
import {SlideShow} from "../Image/SlideShow";
import Toggle 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";
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,
}
}
interface NearbyImageOptions {
lon: number,
lat: number,
radius: number,
maxDaysOld?: 1095,
blacklist: UIEventSource<{url: string}[]>
}
export default class NearbyImages extends VariableUiElement {
constructor(options: NearbyImageOptions) {
const t = Translations.t.image.nearbyPictures
const P4C = require("../../vendor/P4C.min")
const picManager = new P4C.PicturesManager({});
const loadedPictures = UIEventSource.FromPromise<P4CPicture[]>(
picManager.startPicsRetrievalAround(new P4C.LatLng(options.lat, options.lon), options.radius, {
mindate: new Date().getTime() - (options.maxDaysOld ?? 1095) * 24 * 60 * 60 * 1000
})
).map(images => {
console.log("Images are" ,images, "blacklisted is", options.blacklist.data)
images?.sort((a, b) => b.date - a.date)
return images ?.filter(i => !(options.blacklist?.data?.some(blacklisted =>
Mapillary.sameUrl(i.pictureUrl, blacklisted.url)))
&& i.details.isSpherical === false);
}, [options.blacklist])
super(loadedPictures.map(images => {
if(images === undefined){
return new Loading(t.loading);
}
if(images.length === 0){
return t.nothingFound.SetClass("alert block")
}
return new SlideShow(loadedPictures.map(imgs => (imgs ?? []).slice(0, 25).map(i => this.prepareElement(i))))
}));
}
protected prepareElement(info: P4CPicture): BaseUIElement {
const provider = AllImageProviders.byName(info.provider);
return new AttributedImage({url: info.pictureUrl, provider})
}
private 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): Toggle {
const imgNonSelected = this.asAttributedImage(info);
const imageSelected = this.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 Toggle(selected, nonSelected).SetClass("").ToggleOnClick();
}
}
export class SelectOneNearbyImage extends NearbyImages implements InputElement<P4CPicture> {
private readonly value: UIEventSource<P4CPicture>;
constructor(options: NearbyImageOptions & {value?: UIEventSource<P4CPicture>}) {
super(options)
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)
}
})
this.value.addCallback(inf => {
if(inf !== info){
toggle.isEnabled.setData(false)
}
})
return toggle
}
}

View file

@ -2,10 +2,11 @@ import {UIEventSource} from "../../Logic/UIEventSource";
import Translations from "../i18n/Translations";
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
import Toggle from "../Input/Toggle";
import BaseUIElement from "../BaseUIElement";
export class SaveButton extends Toggle {
constructor(value: UIEventSource<any>, osmConnection: OsmConnection) {
constructor(value: UIEventSource<any>, osmConnection: OsmConnection, textEnabled ?: BaseUIElement, textDisabled ?: BaseUIElement) {
if (value === undefined) {
throw "No event source for savebutton, something is wrong"
}
@ -17,9 +18,9 @@ export class SaveButton extends Toggle {
const isSaveable = value.map(v => v !== false && (v ?? "") !== "")
const text = Translations.t.general.save
const saveEnabled = text.Clone().SetClass(`btn`);
const saveDisabled = text.Clone().SetClass(`btn btn-disabled`);
const saveEnabled = (textEnabled ?? Translations.t.general.save.Clone()).SetClass(`btn`);
const saveDisabled = (textDisabled ?? Translations.t.general.save.Clone()).SetClass(`btn btn-disabled`);
const save = new Toggle(
saveEnabled,
saveDisabled,

View file

@ -48,6 +48,13 @@ import {TextField} from "./Input/TextField";
import Wikidata, {WikidataResponse} from "../Logic/Web/Wikidata";
import {Translation} from "./i18n/Translation";
import {AllTagsPanel} from "./AllTagsPanel";
import NearbyImages, {P4CPicture, SelectOneNearbyImage} from "./Popup/NearbyImages";
import Lazy from "./Base/Lazy";
import ChangeTagAction from "../Logic/Osm/Actions/ChangeTagAction";
import {Tag} from "../Logic/Tags/Tag";
import {And} from "../Logic/Tags/And";
import {SaveButton} from "./Popup/SaveButton";
import {MapillaryLink} from "./BigComponents/MapillaryLink";
export interface SpecialVisualization {
funcName: string,
@ -141,6 +148,116 @@ class CloseNoteButton implements SpecialVisualization {
}
class NearbyImageVis implements SpecialVisualization {
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."
}
]
docs = "A component showing nearby images loaded from various online services such as Mapillary. In edit mode and when used on a feature, the user can select an image to add to the feature";
funcName = "nearby_images";
constr(state: FeaturePipelineState, tagSource: UIEventSource<any>, args: string[], guistate: DefaultGuiState): BaseUIElement {
const t = Translations.t.image.nearbyPictures
const mode: "open" | "expandable" | "collapsable" = <any>args[0]
const feature = state.allElements.ContainingFeatures.get(tagSource.data.id)
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);
const nearby = new Lazy(() => {
const alreadyInTheImage = AllImageProviders.LoadImagesFor(tagSource)
const options = {
lon, lat, radius: 50,
value: selectedImage,
blacklist: alreadyInTheImage
};
const slideshow = canBeEdited ? new SelectOneNearbyImage(options) : new NearbyImages(options);
return new Combine([slideshow, new MapillaryLinkVis().constr(state, tagSource, [])])
});
let withEdit: BaseUIElement = nearby;
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,
{
theme: state?.layoutToUse.id,
changeType: "link-image"
}
)
)
};
const saveButton = new SaveButton(selectedImage, state.osmConnection, confirmText, t.noImageSelected)
.onClick(onSave)
withEdit = new Combine([
t.hasMatchingPicture,
nearby,
saveButton
.SetClass("flex justify-end")
]).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
)
}
}
export class MapillaryLinkVis implements SpecialVisualization {
funcName = "mapillary_link"
docs = "Adds a button to open mapillary on the specified location"
args = [{
name: "zoom",
doc: "The startzoom of mapillary",
defaultValue: "18"
}];
public constr(state, tagsSource, args) {
const feat = state.allElements.ContainingFeatures.get(tagsSource.data.id);
const [lon, lat] = GeoOperations.centerpointCoordinates(feat);
let zoom = Number(args[0])
if (isNaN(zoom)) {
zoom = 18
}
return new MapillaryLink({
locationControl: new UIEventSource<Loc>({
lat, lon, zoom
})
})
}
}
export default class SpecialVisualizations {
public static specialVisualizations: SpecialVisualization[] = SpecialVisualizations.init()
@ -309,7 +426,7 @@ export default class SpecialVisualizations {
example: "`{minimap()}`, `{minimap(17, id, _list_of_embedded_feature_ids_calculated_by_calculated_tag):height:10rem; border: 2px solid black}`",
constr: (state, tagSource, args, _) => {
if(state === undefined){
if (state === undefined) {
return undefined
}
const keys = [...args]
@ -940,7 +1057,9 @@ export default class SpecialVisualizations {
}
return new SubstitutedTranslation(title, tagsSource, state)
}))
}
},
new NearbyImageVis(),
new MapillaryLinkVis()
]
specialVisualizations.push(new AutoApplyButton(specialVisualizations))

View file

@ -81,7 +81,7 @@ export class SubstitutedTranslation extends VariableUiElement {
}[] {
for (const knownSpecial of extraMappings.concat(SpecialVisualizations.specialVisualizations)) {
// Note: the '.*?' in the regex reads as 'any character, but in a non-greedy way'
const matched = template.match(`(.*){${knownSpecial.funcName}\\((.*?)\\)(:.*)?}(.*)`);
if (matched != null) {