forked from MapComplete/MapComplete
Add mapillary and other nearby images preview
This commit is contained in:
parent
fc0afbcc18
commit
44223d0f1c
12 changed files with 418 additions and 130 deletions
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue