Add mapillary and other nearby images preview

This commit is contained in:
Pieter Vander Vennet 2022-06-03 01:33:41 +02:00
parent fc0afbcc18
commit 44223d0f1c
12 changed files with 418 additions and 130 deletions

View file

@ -19,7 +19,8 @@ export class CheckBox extends InputElementMap<number[], boolean> {
}
/**
* Supports multi-input
* A list of individual checkboxes
* The value will contain the indexes of the selected checkboxes
*/
export default class CheckBoxes extends InputElement<number[]> {
private static _nextId = 0;
@ -86,9 +87,7 @@ export default class CheckBoxes extends InputElement<number[]> {
formTag.appendChild(wrapper);
value.addCallbackAndRunD((selectedValues) => {
if (selectedValues.indexOf(i) >= 0) {
input.checked = true;
}
input.checked = selectedValues.indexOf(i) >= 0;
if (input.checked) {
wrapper.classList.remove("border-gray-400");

51
UI/Input/Slider.ts Normal file
View file

@ -0,0 +1,51 @@
import {InputElement} from "./InputElement";
import {UIEventSource} from "../../Logic/UIEventSource";
import doc = Mocha.reporters.doc;
export default class Slider extends InputElement<number> {
private readonly _value: UIEventSource<number>
private min: number;
private max: number;
private step: number;
/**
* Constructs a slider input element for natural numbers
* @param min: the minimum value that is allowed, inclusive
* @param max: the max value that is allowed, inclusive
* @param options: value: injectable value; step: the step size of the slider
*/
constructor(min: number, max: number, options?: {
value?: UIEventSource<number>,
step?: 1 | number
}) {
super();
this.max = max;
this.min = min;
this._value = options?.value ?? new UIEventSource<number>(min)
this.step = options?.step ?? 1;
}
GetValue(): UIEventSource<number> {
return this._value;
}
protected InnerConstructElement(): HTMLElement {
const el = document.createElement("input")
el.type = "range"
el.min = "" + this.min
el.max = "" + this.max
el.step = "" + this.step
const valuestore = this._value
el.oninput = () => {
valuestore.setData(Number(el.value))
}
valuestore.addCallbackAndRunD(v => el.value = ""+valuestore.data)
return el;
}
IsValid(t: number): boolean {
return Math.round(t) == t && t >= this.min && t <= this.max;
}
}

View file

@ -12,17 +12,22 @@ 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 {ElementStorage} from "../../Logic/ElementStorage";
import Lazy from "../Base/Lazy";
import {Utils} from "../../Utils";
import beginningOfLine = Mocha.reporters.Base.cursor.beginningOfLine;
export interface P4CPicture {
pictureUrl: string,
date: number,
date?: number,
coordinates: { lat: number, lng: number },
provider: "Mapillary" | string,
author,
license,
detailsUrl: string,
direction,
osmTags: object /*To copy straight into OSM!*/
author?,
license?,
detailsUrl?: string,
direction?,
osmTags?: object /*To copy straight into OSM!*/
,
thumbUrl: string,
details: {
@ -34,56 +39,189 @@ export interface P4CPicture {
export interface NearbyImageOptions {
lon: number,
lat: number,
radius: number,
// Radius of the upstream search
searchRadius?: 500 | number,
maxDaysOld?: 1095 | number,
blacklist: UIEventSource<{url: string}[]>,
blacklist: UIEventSource<{ url: string }[]>,
shownImagesCount?: UIEventSource<number>,
towardscenter?: boolean;
towardscenter?: UIEventSource<boolean>;
allowSpherical?: UIEventSource<boolean>
// Radius of what is shown. Useless to select a value > searchRadius; defaults to searchRadius
shownRadius?: UIEventSource<number>
}
export default class NearbyImages extends VariableUiElement {
class ImagesInLoadedDataFetcher {
private allElements: ElementStorage;
constructor(options: NearbyImageOptions) {
const t = Translations.t.image.nearbyPictures
constructor(state: { allElements: ElementStorage }) {
this.allElements = state.allElements
}
public fetchAround(loc: { lon: number, lat: number, searchRadius?: number }): P4CPicture[] {
const foundImages: P4CPicture[] = []
this.allElements.ContainingFeatures.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?: { allElements: ElementStorage }) {
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))
}
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?: { allElements: ElementStorage }) {
const P4C = require("../../vendor/P4C.min")
const picManager = new P4C.PicturesManager({});
const shownImages = options.shownImagesCount ?? new UIEventSource(25);
const loadedPictures =
UIEventSource.FromPromise<P4CPicture[]>(
picManager.startPicsRetrievalAround(new P4C.LatLng(options.lat, options.lon), options.radius, {
mindate: new Date().getTime() - (options.maxDaysOld ?? (3*365)) * 24 * 60 * 60 * 1000,
towardscenter: options.towardscenter
const searchRadius = options.searchRadius ?? 500;
const nearbyImages = state !== undefined ? new ImagesInLoadedDataFetcher(state).fetchAround(options) : []
return UIEventSource.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 => {
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])
if (images === undefined && nearbyImages.length === 0) {
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 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 => {
const elements = (imgs ?? []).slice(0, shownImages.data).map(i => this.prepareElement(i));
if(imgs !== undefined && elements.length < imgs.length){
// We effectively sliced some items, so we can increase the count
elements.push(loadMoreButton)
const beforeFilterCount = images.length
if (!(options?.allowSpherical?.data)) {
images = images?.filter(i => i.details.isSpherical !== true)
}
return elements;
},[shownImages]);
super(loadedPictures.map(images => {
if(images === undefined){
return new Loading(t.loading);
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(images.length === 0){
return t.nothingFound.SetClass("alert block")
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
})
}
return new SlideShow(imageElements)
}));
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 {
@ -91,14 +229,14 @@ export default class NearbyImages extends VariableUiElement {
return new AttributedImage({url: info.pictureUrl, provider})
}
private asAttributedImage(info: P4CPicture): AttributedImage {
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): Toggle {
const imgNonSelected = this.asAttributedImage(info);
const imageSelected = this.asAttributedImage(info);
protected asToggle(info: P4CPicture): Toggle {
const imgNonSelected = NearbyImages.asAttributedImage(info);
const imageSelected = NearbyImages.asAttributedImage(info);
const nonSelected = new Combine([imgNonSelected]).SetClass("relative block")
const hoveringCheckmark =
@ -117,8 +255,8 @@ export default class NearbyImages extends VariableUiElement {
export class SelectOneNearbyImage extends NearbyImages implements InputElement<P4CPicture> {
private readonly value: UIEventSource<P4CPicture>;
constructor(options: NearbyImageOptions & {value?: UIEventSource<P4CPicture>}) {
super(options)
constructor(options: NearbyImageOptions & { value?: UIEventSource<P4CPicture> }, state?: { allElements: ElementStorage }) {
super(options, state)
this.value = options.value ?? new UIEventSource<P4CPicture>(undefined);
}
@ -135,11 +273,13 @@ export class SelectOneNearbyImage extends NearbyImages implements InputElement<P
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){
if (inf !== info) {
toggle.isEnabled.setData(false)
}
})

View file

@ -55,6 +55,8 @@ import {Tag} from "../Logic/Tags/Tag";
import {And} from "../Logic/Tags/And";
import {SaveButton} from "./Popup/SaveButton";
import {MapillaryLink} from "./BigComponents/MapillaryLink";
import {CheckBox} from "./Input/Checkboxes";
import Slider from "./Input/Slider";
export interface SpecialVisualization {
funcName: string,
@ -174,22 +176,8 @@ class NearbyImageVis implements SpecialVisualization {
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 : NearbyImageOptions & {value}= {
lon, lat, radius: 250,
value: selectedImage,
blacklist: alreadyInTheImage,
towardscenter: false,
maxDaysOld: 365 * 5
};
const slideshow = canBeEdited ? new SelectOneNearbyImage(options) : new NearbyImages(options);
return new Combine([slideshow, new MapillaryLinkVis().constr(state, tagSource, [])])
});
let withEdit: BaseUIElement = nearby;
let saveButton: BaseUIElement = undefined
if (canBeEdited) {
const confirmText: BaseUIElement = new SubstitutedTranslation(t.confirm, tagSource, state)
@ -212,15 +200,45 @@ class NearbyImageVis implements SpecialVisualization {
)
)
};
saveButton = new SaveButton(selectedImage, state.osmConnection, confirmText, t.noImageSelected)
.onClick(onSave).SetClass("flex justify-end")
}
const saveButton = new SaveButton(selectedImage, state.osmConnection, confirmText, t.noImageSelected)
.onClick(onSave)
const nearby = new Lazy(() => {
const towardsCenter = new CheckBox(t.onlyTowards, false)
const radiusValue= state?.osmConnection?.GetPreference("nearby-images-radius","300").map(s => Number(s), [], i => ""+i) ?? new UIEventSource(300);
const radius = new Slider(25, 500, {value:
radiusValue, step: 25})
const alreadyInTheImage = AllImageProviders.LoadImagesFor(tagSource)
const options: NearbyImageOptions & { value } = {
lon, lat,
searchRadius: 500,
shownRadius: radius.GetValue(),
value: selectedImage,
blacklist: alreadyInTheImage,
towardscenter: towardsCenter.GetValue(),
maxDaysOld: 365 * 5
};
const slideshow = canBeEdited ? new SelectOneNearbyImage(options, state) : new NearbyImages(options, state);
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, []).SetClass("mt-6")])
});
let withEdit: BaseUIElement = nearby;
if (canBeEdited) {
withEdit = new Combine([
t.hasMatchingPicture,
nearby,
saveButton
.SetClass("flex justify-end")
nearby
]).SetClass("flex flex-col")
}