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
|
@ -2,6 +2,7 @@
|
|||
* Keeps track of a dictionary 'elementID' -> UIEventSource<tags>
|
||||
*/
|
||||
import {UIEventSource} from "./UIEventSource";
|
||||
import {GeoJSONObject} from "@turf/turf";
|
||||
|
||||
export class ElementStorage {
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import {BBox} from "./BBox";
|
|||
import togpx from "togpx"
|
||||
import Constants from "../Models/Constants";
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
|
||||
import {Coord} from "@turf/turf";
|
||||
|
||||
export class GeoOperations {
|
||||
|
||||
|
@ -729,6 +730,12 @@ export class GeoOperations {
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes two points and finds the geographic bearing between them, i.e. the angle measured in degrees from the north line (0 degrees)
|
||||
*/
|
||||
public static bearing(a: Coord, b: Coord): number {
|
||||
return turf.bearing(a, b)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -26,7 +26,6 @@ export class Mapillary extends ImageProvider {
|
|||
return true
|
||||
}
|
||||
try {
|
||||
console.log("COmparing",a,b)
|
||||
const aUrl = new URL(a)
|
||||
const bUrl = new URL(b)
|
||||
if (aUrl.host !== bUrl.host || aUrl.pathname !== bUrl.pathname) {
|
||||
|
@ -46,7 +45,7 @@ console.log("COmparing",a,b)
|
|||
return allSame;
|
||||
|
||||
} catch (e) {
|
||||
Console.debug("Could not compare ", a, "and", b, "due to", e)
|
||||
console.debug("Could not compare ", a, "and", b, "due to", e)
|
||||
}
|
||||
return false;
|
||||
|
||||
|
|
|
@ -122,8 +122,8 @@ export class OsmConnection {
|
|||
return new ChangesetHandler(this._dryRun, this, allElements, changes, this.auth);
|
||||
}
|
||||
|
||||
public GetPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> {
|
||||
return this.preferencesHandler.GetPreference(key, prefix);
|
||||
public GetPreference(key: string, defaultValue: string = undefined, prefix: string = "mapcomplete-"): UIEventSource<string> {
|
||||
return this.preferencesHandler.GetPreference(key, defaultValue, prefix);
|
||||
}
|
||||
|
||||
public GetLongPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> {
|
||||
|
|
|
@ -106,7 +106,7 @@ export class OsmPreferences {
|
|||
return source;
|
||||
}
|
||||
|
||||
public GetPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> {
|
||||
public GetPreference(key: string, defaultValue : string = undefined, prefix: string = "mapcomplete-"): UIEventSource<string> {
|
||||
key = prefix + key;
|
||||
key = key.replace(/[:\\\/"' {}.%]/g, '')
|
||||
if (key.length >= 255) {
|
||||
|
@ -120,7 +120,7 @@ export class OsmPreferences {
|
|||
this.UpdatePreferences();
|
||||
}
|
||||
|
||||
const pref = new UIEventSource<string>(this.preferences.data[key], "osm-preference:" + key);
|
||||
const pref = new UIEventSource<string>(this.preferences.data[key] ?? defaultValue, "osm-preference:" + key);
|
||||
pref.addCallback((v) => {
|
||||
this.UploadPreference(key, v);
|
||||
});
|
||||
|
|
|
@ -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
51
UI/Input/Slider.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
49
assets/svg/filter_disable.svg
Normal file
49
assets/svg/filter_disable.svg
Normal file
|
@ -0,0 +1,49 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="328px"
|
||||
height="374px"
|
||||
viewBox="0 0 328 374"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
sodipodi:docname="filter_disable.svg"
|
||||
inkscape:version="1.1.2 (1:1.1+202202050950+0a00cf5339)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs9" />
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:pageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="2.2486631"
|
||||
inkscape:cx="164.0975"
|
||||
inkscape:cy="187.22235"
|
||||
inkscape:current-layer="svg5" />
|
||||
<g
|
||||
id="surface1">
|
||||
<path
|
||||
style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;"
|
||||
d="M 204.175781 186.890625 L 204.175781 350.699219 C 204.683594 353.851562 204.40625 357.082031 203.367188 360.09375 C 202.328125 363.105469 200.5625 365.8125 198.230469 367.957031 C 194.390625 371.824219 189.195312 374 183.78125 374 C 178.363281 374 173.171875 371.824219 169.332031 367.957031 L 128.335938 326.378906 C 126.113281 324.152344 124.421875 321.4375 123.390625 318.445312 C 122.363281 315.453125 122.019531 312.269531 122.390625 309.121094 L 122.390625 187.09375 L 4.328125 33.679688 C 1 29.34375 -0.503906 23.851562 0.148438 18.398438 C 0.800781 12.945312 3.558594 7.976562 7.8125 4.578125 C 11.417969 1.644531 15.898438 0.0273438 20.519531 0 L 307.480469 0 C 312.105469 0.0273438 316.582031 1.644531 320.1875 4.578125 C 324.445312 7.976562 327.199219 12.945312 327.851562 18.398438 C 328.503906 23.851562 327 29.34375 323.671875 33.679688 L 205.609375 187.09375 Z M 204.175781 186.890625 "
|
||||
id="path2" />
|
||||
</g>
|
||||
<g
|
||||
id="surface1-3"
|
||||
transform="matrix(1.2223339,0,0,1.2223339,-152.85378,-266.10503)"
|
||||
style="fill:#ff0000;fill-opacity:1">
|
||||
<path
|
||||
style="fill:#ff0000;fill-opacity:1;stroke:#b40000;stroke-width:34.2679;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1"
|
||||
d="M 364.47831,491.73933 252.63509,379.89612"
|
||||
id="path826" />
|
||||
<path
|
||||
style="fill:#ff0000;fill-opacity:1;stroke:#b40000;stroke-width:34.2684;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1"
|
||||
d="M 364.64638,379.35739 252.80473,491.199"
|
||||
id="path828" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.5 KiB |
|
@ -33,9 +33,7 @@ Use a more readable tab size (opinionated).
|
|||
*/
|
||||
|
||||
html {
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
tab-size: 4;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -465,7 +463,7 @@ textarea {
|
|||
resize: vertical;
|
||||
}
|
||||
|
||||
input::-moz-placeholder, textarea::-moz-placeholder {
|
||||
input::-webkit-input-placeholder, textarea::-webkit-input-placeholder {
|
||||
opacity: 1;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
@ -872,6 +870,10 @@ video {
|
|||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.mt-6 {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.mt-1 {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
@ -1162,7 +1164,6 @@ video {
|
|||
|
||||
.w-min {
|
||||
width: -webkit-min-content;
|
||||
width: -moz-min-content;
|
||||
width: min-content;
|
||||
}
|
||||
|
||||
|
@ -1172,7 +1173,6 @@ video {
|
|||
|
||||
.w-max {
|
||||
width: -webkit-max-content;
|
||||
width: -moz-max-content;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
|
@ -1186,7 +1186,6 @@ video {
|
|||
|
||||
.min-w-min {
|
||||
min-width: -webkit-min-content;
|
||||
min-width: -moz-min-content;
|
||||
min-width: min-content;
|
||||
}
|
||||
|
||||
|
@ -1219,18 +1218,21 @@ video {
|
|||
}
|
||||
|
||||
.transform {
|
||||
transform: var(--tw-transform);
|
||||
-webkit-transform: var(--tw-transform);
|
||||
transform: var(--tw-transform);
|
||||
}
|
||||
|
||||
@-webkit-keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1452,11 +1454,6 @@ video {
|
|||
border-color: rgba(229, 231, 235, var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-green-500 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgba(16, 185, 129, var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-opacity-50 {
|
||||
--tw-border-opacity: 0.5;
|
||||
}
|
||||
|
@ -1774,31 +1771,36 @@ video {
|
|||
|
||||
.blur {
|
||||
--tw-blur: blur(8px);
|
||||
filter: var(--tw-filter);
|
||||
-webkit-filter: var(--tw-filter);
|
||||
filter: var(--tw-filter);
|
||||
}
|
||||
|
||||
.drop-shadow {
|
||||
--tw-drop-shadow: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1)) drop-shadow(0 1px 1px rgba(0, 0, 0, 0.06));
|
||||
filter: var(--tw-filter);
|
||||
-webkit-filter: var(--tw-filter);
|
||||
filter: var(--tw-filter);
|
||||
}
|
||||
|
||||
.invert {
|
||||
--tw-invert: invert(100%);
|
||||
filter: var(--tw-filter);
|
||||
-webkit-filter: var(--tw-filter);
|
||||
filter: var(--tw-filter);
|
||||
}
|
||||
|
||||
.filter {
|
||||
filter: var(--tw-filter);
|
||||
-webkit-filter: var(--tw-filter);
|
||||
filter: var(--tw-filter);
|
||||
}
|
||||
|
||||
.\!filter {
|
||||
filter: var(--tw-filter) !important;
|
||||
-webkit-filter: var(--tw-filter) !important;
|
||||
filter: var(--tw-filter) !important;
|
||||
}
|
||||
|
||||
.transition {
|
||||
transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
|
||||
transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, -webkit-transform, -webkit-filter, -webkit-backdrop-filter;
|
||||
transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
|
||||
transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;
|
||||
transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-transform, -webkit-filter, -webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
|
@ -1979,16 +1981,19 @@ a {
|
|||
|
||||
.btn-secondary {
|
||||
background-color: var(--catch-detail-color);
|
||||
filter: saturate(0.5);
|
||||
-webkit-filter: saturate(0.5);
|
||||
filter: saturate(0.5);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: var(--catch-detail-color);
|
||||
filter: unset;
|
||||
-webkit-filter: unset;
|
||||
filter: unset;
|
||||
}
|
||||
|
||||
.btn-disabled {
|
||||
filter: saturate(0.3);
|
||||
-webkit-filter: saturate(0.3);
|
||||
filter: saturate(0.3);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
|
@ -1998,7 +2003,6 @@ a {
|
|||
|
||||
.h-min {
|
||||
height: -webkit-min-content;
|
||||
height: -moz-min-content;
|
||||
height: min-content;
|
||||
}
|
||||
|
||||
|
@ -2008,7 +2012,6 @@ a {
|
|||
|
||||
.w-min {
|
||||
width: -webkit-min-content;
|
||||
width: -moz-min-content;
|
||||
width: min-content;
|
||||
}
|
||||
|
||||
|
@ -2180,7 +2183,6 @@ li::marker {
|
|||
.invalid {
|
||||
box-shadow: 0 0 10px #ff5353;
|
||||
height: -webkit-min-content;
|
||||
height: -moz-min-content;
|
||||
height: min-content;
|
||||
}
|
||||
|
||||
|
@ -2234,11 +2236,13 @@ li::marker {
|
|||
/* This is the animation on the marker to add a new point - it slides through all the possible presets */
|
||||
|
||||
from {
|
||||
transform: translateX(0%);
|
||||
-webkit-transform: translateX(0%);
|
||||
transform: translateX(0%);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateX(calc(-100% + 42px));
|
||||
-webkit-transform: translateX(calc(-100% + 42px));
|
||||
transform: translateX(calc(-100% + 42px));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2246,18 +2250,21 @@ li::marker {
|
|||
/* This is the animation on the marker to add a new point - it slides through all the possible presets */
|
||||
|
||||
from {
|
||||
transform: translateX(0%);
|
||||
-webkit-transform: translateX(0%);
|
||||
transform: translateX(0%);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateX(calc(-100% + 42px));
|
||||
-webkit-transform: translateX(calc(-100% + 42px));
|
||||
transform: translateX(calc(-100% + 42px));
|
||||
}
|
||||
}
|
||||
|
||||
.hand-drag-animation {
|
||||
-webkit-animation: hand-drag-animation 6s ease-in-out infinite;
|
||||
animation: hand-drag-animation 6s ease-in-out infinite;
|
||||
transform-origin: 50% 125%;
|
||||
-webkit-transform-origin: 50% 125%;
|
||||
transform-origin: 50% 125%;
|
||||
}
|
||||
|
||||
@-webkit-keyframes hand-drag-animation {
|
||||
|
@ -2265,37 +2272,44 @@ li::marker {
|
|||
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: rotate(-30deg);
|
||||
-webkit-transform: rotate(-30deg);
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
|
||||
6% {
|
||||
opacity: 1;
|
||||
transform: rotate(-30deg);
|
||||
-webkit-transform: rotate(-30deg);
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
|
||||
12% {
|
||||
opacity: 1;
|
||||
transform: rotate(-45deg);
|
||||
-webkit-transform: rotate(-45deg);
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
24% {
|
||||
opacity: 1;
|
||||
transform: rotate(-00deg);
|
||||
-webkit-transform: rotate(-00deg);
|
||||
transform: rotate(-00deg);
|
||||
}
|
||||
|
||||
30% {
|
||||
opacity: 1;
|
||||
transform: rotate(-30deg);
|
||||
-webkit-transform: rotate(-30deg);
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
|
||||
36% {
|
||||
opacity: 0;
|
||||
transform: rotate(-30deg);
|
||||
-webkit-transform: rotate(-30deg);
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: rotate(-30deg);
|
||||
-webkit-transform: rotate(-30deg);
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2304,37 +2318,44 @@ li::marker {
|
|||
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: rotate(-30deg);
|
||||
-webkit-transform: rotate(-30deg);
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
|
||||
6% {
|
||||
opacity: 1;
|
||||
transform: rotate(-30deg);
|
||||
-webkit-transform: rotate(-30deg);
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
|
||||
12% {
|
||||
opacity: 1;
|
||||
transform: rotate(-45deg);
|
||||
-webkit-transform: rotate(-45deg);
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
24% {
|
||||
opacity: 1;
|
||||
transform: rotate(-00deg);
|
||||
-webkit-transform: rotate(-00deg);
|
||||
transform: rotate(-00deg);
|
||||
}
|
||||
|
||||
30% {
|
||||
opacity: 1;
|
||||
transform: rotate(-30deg);
|
||||
-webkit-transform: rotate(-30deg);
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
|
||||
36% {
|
||||
opacity: 0;
|
||||
transform: rotate(-30deg);
|
||||
-webkit-transform: rotate(-30deg);
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: rotate(-30deg);
|
||||
-webkit-transform: rotate(-30deg);
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2665,7 +2686,6 @@ input {
|
|||
|
||||
.md\:w-max {
|
||||
width: -webkit-max-content;
|
||||
width: -moz-max-content;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
|
|
|
@ -277,6 +277,7 @@
|
|||
"dontDelete": "Cancel",
|
||||
"isDeleted": "Deleted",
|
||||
"nearbyPictures": {
|
||||
"allFiltered": "No images matched your filter",
|
||||
"browseNearby": "Browse nearby images...",
|
||||
"confirm": "The selected image shows {title()}",
|
||||
"hasMatchingPicture": "Does a picture match the object? Select it below",
|
||||
|
@ -284,7 +285,10 @@
|
|||
"loading": "Loading nearby images...",
|
||||
"noImageSelected": "Select an image to link it to the object",
|
||||
"nothingFound": "No nearby images found...",
|
||||
"title": "Nearby pictures"
|
||||
"onlyTowards": "Only show pictures which are taken towards this object",
|
||||
"removeFilters": "Click here to remove the filters",
|
||||
"title": "Nearby pictures",
|
||||
"withinRadius": "Only show pictures which are taken within {radius} meter of this object"
|
||||
},
|
||||
"pleaseLogin": "Please log in to add a picture",
|
||||
"respectPrivacy": "Do not photograph people nor license plates. Do not upload Google Maps, Google Streetview or other copyrighted sources.",
|
||||
|
|
Loading…
Reference in a new issue