Merge branch 'develop' into feature/json-editor

This commit is contained in:
Robin van der Linde 2024-02-22 02:24:17 +01:00
commit e932bfd9cd
Signed by untrusted user: Robin-van-der-Linde
GPG key ID: 53956B3252478F0D
201 changed files with 4529 additions and 4456 deletions

View file

@ -45,12 +45,7 @@ export default class LayoutSource extends FeatureSourceMerger {
})
)
const overpassSource = LayoutSource.setupOverpass(
osmLayers,
bounds,
zoom,
featureSwitches
)
const overpassSource = LayoutSource.setupOverpass(osmLayers, bounds, zoom, featureSwitches)
const osmApiSource = LayoutSource.setupOsmApiSource(
osmLayers,

View file

@ -345,15 +345,18 @@ export class GeoOperations {
return <any>way
}
public static toCSV(features: Feature[] | FeatureCollection, options?: {
ignoreTags?: RegExp
}): string {
public static toCSV(
features: Feature[] | FeatureCollection,
options?: {
ignoreTags?: RegExp
}
): string {
const headerValuesSeen = new Set<string>()
const headerValuesOrdered: string[] = []
function addH(key: string) {
if(options?.ignoreTags){
if(key.match(options.ignoreTags)){
if (options?.ignoreTags) {
if (key.match(options.ignoreTags)) {
return
}
}
@ -746,7 +749,14 @@ export class GeoOperations {
*/
public static featureToCoordinateWithRenderingType(
feature: Feature,
location: "point" | "centroid" | "start" | "end" | "projected_centerpoint" | string
location:
| "point"
| "centroid"
| "start"
| "end"
| "projected_centerpoint"
| "polygon_centerpoint"
| string
): [number, number] | undefined {
switch (location) {
case "point":
@ -759,6 +769,11 @@ export class GeoOperations {
return undefined
}
return GeoOperations.centerpointCoordinates(feature)
case "polygon_centroid":
if (feature.geometry.type === "Polygon") {
return GeoOperations.centerpointCoordinates(feature)
}
return undefined
case "projected_centerpoint":
if (
feature.geometry.type === "LineString" ||
@ -783,7 +798,7 @@ export class GeoOperations {
}
return undefined
default:
throw "Unkown location type: " + location
throw "Unkown location type: " + location+" for feature "+feature.properties.id
}
}

View file

@ -10,6 +10,8 @@ import { WikidataImageProvider } from "./WikidataImageProvider"
* A generic 'from the interwebz' image picker, without attribution
*/
export default class AllImageProviders {
private static dontLoadFromPrefixes = ["https://photos.app.goo.gl/"]
public static ImageAttributionSource: ImageProvider[] = [
Imgur.singleton,
Mapillary.singleton,
@ -19,7 +21,8 @@ export default class AllImageProviders {
[].concat(
...Imgur.defaultValuePrefix,
...WikimediaImageProvider.commonsPrefixes,
...Mapillary.valuePrefixes
...Mapillary.valuePrefixes,
...AllImageProviders.dontLoadFromPrefixes
)
),
]

View file

@ -25,7 +25,7 @@ export default class MetaTagging {
>()
constructor(state: {
readonly selectedElementAndLayer: Store<{ feature: Feature; layer: LayerConfig }>
readonly selectedElement: Store<Feature>
readonly layout: LayoutConfig
readonly osmObjectDownloader: OsmObjectDownloader
readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>
@ -61,7 +61,8 @@ export default class MetaTagging {
})
}
state.selectedElementAndLayer.addCallbackAndRunD(({ feature, layer }) => {
state.selectedElement.addCallbackAndRunD((feature) => {
const layer = state.layout.getMatchingLayer(feature.properties)
// Force update the tags of the currently selected element
MetaTagging.addMetatags(
[feature],

View file

@ -73,7 +73,6 @@ export default class UserRelatedState {
constructor(
osmConnection: OsmConnection,
availableLanguages?: string[],
layout?: LayoutConfig,
featureSwitches?: FeatureSwitchState,
mapProperties?: MapProperties
@ -115,7 +114,8 @@ export default class UserRelatedState {
)
this.mangroveIdentity = new MangroveIdentity(
this.osmConnection.GetLongPreference("identity", "mangrove")
this.osmConnection.GetLongPreference("identity", "mangrove"),
this.osmConnection.GetPreference("identity-creation-date", "mangrove")
)
this.preferredBackgroundLayer = this.osmConnection.GetPreference(
"preferred-background-layer",
@ -365,6 +365,11 @@ export default class UserRelatedState {
[translationMode]
)
this.mangroveIdentity.getKeyId().addCallbackAndRun((kid) => {
amendedPrefs.data["mangrove_kid"] = kid
amendedPrefs.ping()
})
const usersettingMetaTagging = new ThemeMetaTagging()
osmConnection.userDetails.addCallback((userDetails) => {
for (const k in userDetails) {

View file

@ -432,6 +432,6 @@ export class And extends TagsFilter {
}
asMapboxExpression(): ExpressionSpecification {
return ["all", ...this.and.map(t => t.asMapboxExpression())]
return ["all", ...this.and.map((t) => t.asMapboxExpression())]
}
}

View file

@ -13,7 +13,7 @@ export default class ComparingTag implements TagsFilter {
key: string,
predicate: (value: string | undefined) => boolean,
representation: "<" | ">" | "<=" | ">=",
boundary: string,
boundary: string
) {
this._key = key
this._predicate = predicate

View file

@ -291,6 +291,6 @@ export class Or extends TagsFilter {
}
asMapboxExpression(): ExpressionSpecification {
return ["any", ...this.or.map(t => t.asMapboxExpression())]
return ["any", ...this.or.map((t) => t.asMapboxExpression())]
}
}

View file

@ -360,8 +360,8 @@ export class RegexTag extends TagsFilter {
}
asMapboxExpression(): ExpressionSpecification {
if(typeof this.key=== "string" && typeof this.value === "string" ) {
return [this.invert ? "!=" : "==", ["get",this.key], this.value]
if (typeof this.key === "string" && typeof this.value === "string") {
return [this.invert ? "!=" : "==", ["get", this.key], this.value]
}
throw "TODO"
}

View file

@ -65,7 +65,7 @@ export class Tag extends TagsFilter {
asOverpass(): string[] {
if (this.value === "") {
// NOT having this key
return ["[!\"" + this.key + "\"]"]
return ['[!"' + this.key + '"]']
}
return [`["${this.key}"="${this.value}"]`]
}
@ -83,7 +83,7 @@ export class Tag extends TagsFilter {
asHumanString(
linkToWiki?: boolean,
shorten?: boolean,
currentProperties?: Record<string, string>,
currentProperties?: Record<string, string>
) {
let v = this.value
if (typeof v !== "string") {
@ -170,12 +170,7 @@ export class Tag extends TagsFilter {
asMapboxExpression(): ExpressionSpecification {
if (this.value === "") {
return [
"any",
["!", ["has", this.key]],
["==", ["get", this.key], ""],
]
return ["any", ["!", ["has", this.key]], ["==", ["get", this.key], ""]]
}
return ["==", ["get", this.key], this.value]
}

View file

@ -306,7 +306,8 @@ export abstract class Store<T> implements Readable<T> {
export class ImmutableStore<T> extends Store<T> {
public readonly data: T
static FALSE = new ImmutableStore<boolean>(false)
static TRUE = new ImmutableStore<boolean>(true)
constructor(data: T) {
super()
this.data = data

View file

@ -5,10 +5,17 @@ import { Feature, Position } from "geojson"
import { GeoOperations } from "../GeoOperations"
export class MangroveIdentity {
public readonly keypair: Store<CryptoKeyPair>
public readonly key_id: Store<string>
private readonly keypair: Store<CryptoKeyPair>
/**
* Same as the one in the user settings
*/
public readonly mangroveIdentity: UIEventSource<string>
private readonly key_id: Store<string>
private readonly _mangroveIdentityCreationDate: UIEventSource<string>
constructor(mangroveIdentity: UIEventSource<string>) {
constructor(mangroveIdentity: UIEventSource<string>, mangroveIdentityCreationDate: UIEventSource<string>) {
this.mangroveIdentity = mangroveIdentity
this._mangroveIdentityCreationDate = mangroveIdentityCreationDate
const key_id = new UIEventSource<string>(undefined)
this.key_id = key_id
const keypairEventSource = new UIEventSource<CryptoKeyPair>(undefined)
@ -22,14 +29,6 @@ export class MangroveIdentity {
const pem = await MangroveReviews.publicToPem(keypair.publicKey)
key_id.setData(pem)
})
try {
if (!Utils.runningFromConsole && (mangroveIdentity.data ?? "") === "") {
MangroveIdentity.CreateIdentity(mangroveIdentity).then((_) => {})
}
} catch (e) {
console.error("Could not create identity: ", e)
}
}
/**
@ -37,14 +36,71 @@ export class MangroveIdentity {
* Is written into the UIEventsource, which was passed into the constructor
* @constructor
*/
private static async CreateIdentity(identity: UIEventSource<string>): Promise<void> {
private async CreateIdentity(): Promise<void> {
const keypair = await MangroveReviews.generateKeypair()
const jwk = await MangroveReviews.keypairToJwk(keypair)
if ((identity.data ?? "") !== "") {
if ((this.mangroveIdentity.data ?? "") !== "") {
// Identity has been loaded via osmPreferences by now - we don't overwrite
return
}
identity.setData(JSON.stringify(jwk))
console.log("Creating a new Mangrove identity!")
this.mangroveIdentity.setData(JSON.stringify(jwk))
this._mangroveIdentityCreationDate.setData(new Date().toISOString())
}
/**
* Only called to create a review.
*/
async getKeypair(): Promise<CryptoKeyPair> {
if (this.keypair.data ?? "" === "") {
// We want to create a review, but it seems like no key has been setup at this moment
// We create the key
try {
if (!Utils.runningFromConsole && (this.mangroveIdentity.data ?? "") === "") {
await this.CreateIdentity()
}
} catch (e) {
console.error("Could not create identity: ", e)
}
}
return this.keypair.data
}
getKeyId(): Store<string> {
return this.key_id
}
private allReviewsById: UIEventSource<(Review & { kid: string; signature: string })[]> =
undefined
/**
* Gets all reviews that are made for the current identity.
*/
public getAllReviews(): Store<(Review & { kid: string; signature: string })[]> {
if (this.allReviewsById !== undefined) {
return this.allReviewsById
}
this.allReviewsById = new UIEventSource([])
this.key_id.map((pem) => {
if (pem === undefined) {
return []
}
MangroveReviews.getReviews({
kid: pem,
}).then((allReviews) => {
this.allReviewsById.setData(
allReviews.reviews.map((r) => ({
...r,
...r.payload,
}))
)
})
})
return this.allReviewsById
}
addReview(review: Review & { kid; signature }) {
this.allReviewsById?.setData(this.allReviewsById?.data?.concat([review]))
}
}
@ -73,7 +129,7 @@ export default class FeatureReviews {
private constructor(
feature: Feature,
tagsSource: UIEventSource<Record<string, string>>,
mangroveIdentity?: MangroveIdentity,
mangroveIdentity: MangroveIdentity,
options?: {
nameKey?: "name" | string
fallbackName?: string
@ -82,8 +138,7 @@ export default class FeatureReviews {
) {
const centerLonLat = GeoOperations.centerpointCoordinates(feature)
;[this._lon, this._lat] = centerLonLat
this._identity =
mangroveIdentity ?? new MangroveIdentity(new UIEventSource<string>(undefined))
this._identity = mangroveIdentity
const nameKey = options?.nameKey ?? "name"
if (feature.geometry.type === "Point") {
@ -176,24 +231,33 @@ export default class FeatureReviews {
* The given review is uploaded to mangrove.reviews and added to the list of known reviews
*/
public async createReview(review: Omit<Review, "sub">): Promise<void> {
if(review.opinion.length > FeatureReviews .REVIEW_OPINION_MAX_LENGTH){
throw "Opinion too long, should be at most "+FeatureReviews.REVIEW_OPINION_MAX_LENGTH+" characters long"
if (
review.opinion !== undefined &&
review.opinion.length > FeatureReviews.REVIEW_OPINION_MAX_LENGTH
) {
throw (
"Opinion too long, should be at most " +
FeatureReviews.REVIEW_OPINION_MAX_LENGTH +
" characters long"
)
}
const r: Review = {
sub: this.subjectUri.data,
...review,
}
const keypair: CryptoKeyPair = this._identity.keypair.data
const keypair: CryptoKeyPair = await this._identity.getKeypair()
const jwt = await MangroveReviews.signReview(keypair, r)
const kid = await MangroveReviews.publicToPem(keypair.publicKey)
await MangroveReviews.submitReview(jwt)
this._reviews.data.push({
const reviewWithKid = {
...r,
kid,
signature: jwt,
madeByLoggedInUser: new ImmutableStore(true),
})
}
this._reviews.data.push(reviewWithKid)
this._reviews.ping()
this._identity.addReview(reviewWithKid)
}
/**
@ -235,7 +299,7 @@ export default class FeatureReviews {
...review,
kid: reviewData.kid,
signature: reviewData.signature,
madeByLoggedInUser: this._identity.key_id.map((user_key_id) => {
madeByLoggedInUser: this._identity.getKeyId().map((user_key_id) => {
return reviewData.kid === user_key_id
}),
})

View file

@ -115,7 +115,6 @@ export default class ThemeViewStateHashActor {
""
)
selectedElement.setData(found)
state.selectedLayer.setData(layer)
return true
}

View file

@ -52,7 +52,9 @@ export class AvailableRasterLayers {
type: "Feature",
properties: {
name: "MapTiler",
url: "https://api.maptiler.com/maps/15cc8f61-0353-4be6-b8da-13daea5f7432/style.json?key="+Constants.maptilerApiKey,
url:
"https://api.maptiler.com/maps/15cc8f61-0353-4be6-b8da-13daea5f7432/style.json?key=" +
Constants.maptilerApiKey,
category: "osmbasedmap",
id: "maptiler",
type: "vector",

View file

@ -156,7 +156,7 @@ export class On<P, T> extends DesugaringStep<T> {
convert(json: T, context: ConversionContext): T {
const key = this.key
const value: P = json[key]
const value: P = json?.[key]
if (value === undefined || value === null) {
return json
}

View file

@ -366,7 +366,7 @@ class AddDependencyLayersToTheme extends DesugaringStep<LayoutConfigJson> {
themeId: string
): { config: LayerConfigJson; reason: string }[] {
const dependenciesToAdd: { config: LayerConfigJson; reason: string }[] = []
const loadedLayerIds: Set<string> = new Set<string>(alreadyLoaded.map((l) => l.id))
const loadedLayerIds: Set<string> = new Set<string>(alreadyLoaded.map((l) => l?.id))
// Verify cross-dependencies
let unmetDependencies: {
@ -565,25 +565,39 @@ class PostvalidateTheme extends DesugaringStep<LayoutConfigJson> {
convert(json: LayoutConfigJson, context: ConversionContext): LayoutConfigJson {
for (const l of json.layers) {
const layer = <LayerConfigJson> l
const basedOn = <string> layer["_basedOn"]
const layer = <LayerConfigJson>l
const basedOn = <string>layer["_basedOn"]
const basedOnDef = this._state.sharedLayers.get(basedOn)
if(!basedOn){
if (!basedOn) {
continue
}
if(layer["name"] === null){
if (layer["name"] === null) {
continue
}
const sameBasedOn = <LayerConfigJson[]> json.layers.filter(l => l["_basedOn"] === layer["_basedOn"] && l["id"] !== layer.id)
const minZoomAll = Math.min(...sameBasedOn.map(sbo => sbo.minzoom))
const sameBasedOn = <LayerConfigJson[]>(
json.layers.filter(
(l) => l["_basedOn"] === layer["_basedOn"] && l["id"] !== layer.id
)
)
const minZoomAll = Math.min(...sameBasedOn.map((sbo) => sbo.minzoom))
const sameNameDetected = sameBasedOn.some( same => JSON.stringify(layer["name"]) === JSON.stringify(same["name"]))
if(!sameNameDetected){
const sameNameDetected = sameBasedOn.some(
(same) => JSON.stringify(layer["name"]) === JSON.stringify(same["name"])
)
if (!sameNameDetected) {
// The name is unique, so it'll won't be confusing
continue
}
if(minZoomAll < layer.minzoom){
context.err("There are multiple layers based on "+basedOn+". The layer with id "+layer.id+" has a minzoom of "+layer.minzoom+", and has a name set. Another similar layer has a lower minzoom. As such, the layer selection might show 'zoom in to see features' even though some of the features are already visible. Set `\"name\": null` for this layer and eventually remove the 'name':null for the other layer.")
if (minZoomAll < layer.minzoom) {
context.err(
"There are multiple layers based on " +
basedOn +
". The layer with id " +
layer.id +
" has a minzoom of " +
layer.minzoom +
", and has a name set. Another similar layer has a lower minzoom. As such, the layer selection might show 'zoom in to see features' even though some of the features are already visible. Set `\"name\": null` for this layer and eventually remove the 'name':null for the other layer."
)
}
}

View file

@ -907,8 +907,12 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
)
}
if(json.icon?.["size"]){
context.enters("icon","size").err("size is not a valid attribute. Did you mean 'class'? Class can be one of `small`, `medium` or `large`")
if (json.icon?.["size"]) {
context
.enters("icon", "size")
.err(
"size is not a valid attribute. Did you mean 'class'? Class can be one of `small`, `medium` or `large`"
)
}
if (json.freeform) {

View file

@ -82,5 +82,5 @@ export default interface LineRenderingConfigJson {
* suggestions: [{if: "./assets/png/oneway.png", then: "Show a oneway error"}]
* type: image
*/
imageAlongWay?: {if: TagConfigJson, then: string}[] | string
imageAlongWay?: { if: TagConfigJson; then: string }[] | string
}

View file

@ -28,9 +28,17 @@ export default interface PointRenderingConfigJson {
/**
* question: At what location should this icon be shown?
* multianswer: true
* suggestions: return [{if: "value=point",then: "Show an icon for point (node) objects"},{if: "value=centroid",then: "Show an icon for line or polygon (way) objects at their centroid location"}, {if: "value=start",then: "Show an icon for line (way) objects at the start"},{if: "value=end",then: "Show an icon for line (way) object at the end"},{if: "value=projected_centerpoint",then: "Show an icon for line (way) object near the centroid location, but moved onto the line"}]
* suggestions: return [{if: "value=point",then: "Show an icon for point (node) objects"},{if: "value=centroid",then: "Show an icon for line or polygon (way) objects at their centroid location"}, {if: "value=start",then: "Show an icon for line (way) objects at the start"},{if: "value=end",then: "Show an icon for line (way) object at the end"},{if: "value=projected_centerpoint",then: "Show an icon for line (way) object near the centroid location, but moved onto the line. Does not show an item on polygons"}, ,{if: "value=polygon_centroid",then: "Show an icon at a polygon centroid (but not if it is a way)"}]
*/
location: ("point" | "centroid" | "start" | "end" | "projected_centerpoint" | string)[]
location: (
| "point"
| "centroid"
| "start"
| "end"
| "projected_centerpoint"
| "polygon_centroid"
| string
)[]
/**
* The marker for an element.

View file

@ -467,7 +467,7 @@ export default class LayerConfig extends WithContextLoader {
new Link(
Utils.runningFromConsole
? "<img src='https://mapcomplete.org/assets/svg/statistics.svg' height='18px'>"
: new SvelteUIElement(Statistics, {class: "w-4 h-4 mr-2"}),
: new SvelteUIElement(Statistics, { class: "w-4 h-4 mr-2" }),
"https://taginfo.openstreetmap.org/keys/" + values.key + "#values",
true
),

View file

@ -13,7 +13,7 @@ export default class LineRenderingConfig extends WithContextLoader {
public readonly fill: TagRenderingConfig
public readonly fillColor: TagRenderingConfig
public readonly leftRightSensitive: boolean
public readonly imageAlongWay: { if?: TagsFilter, then: string }[]
public readonly imageAlongWay: { if?: TagsFilter; then: string }[]
constructor(json: LineRenderingConfigJson, context: string) {
super(json, context)
@ -33,15 +33,13 @@ export default class LineRenderingConfig extends WithContextLoader {
for (let i = 0; i < json.imageAlongWay.length; i++) {
const imgAlong = json.imageAlongWay[i]
const ctx = context + ".imageAlongWay[" + i + "]"
if(!imgAlong.then.endsWith(".png")){
if (!imgAlong.then.endsWith(".png")) {
throw "An imageAlongWay should always be a PNG image"
}
this.imageAlongWay.push(
{
if: TagUtils.Tag(imgAlong.if, ctx),
then: imgAlong.then,
},
)
this.imageAlongWay.push({
if: TagUtils.Tag(imgAlong.if, ctx),
then: imgAlong.then,
})
}
}
}

View file

@ -38,9 +38,16 @@ export default class PointRenderingConfig extends WithContextLoader {
"start",
"end",
"projected_centerpoint",
"polygon_centroid",
])
public readonly location: Set<
"point" | "centroid" | "start" | "end" | "projected_centerpoint" | string
| "point"
| "centroid"
| "start"
| "end"
| "projected_centerpoint"
| "polygon_centroid"
| string
>
public readonly marker: IconConfig[]

View file

@ -84,7 +84,6 @@ export default class ThemeViewState implements SpecialVisualizationState {
readonly osmConnection: OsmConnection
readonly selectedElement: UIEventSource<Feature>
readonly selectedElementAndLayer: Store<{ feature: Feature; layer: LayerConfig }>
readonly mapProperties: MapLibreAdaptor & MapProperties & ExportableMap
readonly osmObjectDownloader: OsmObjectDownloader
@ -112,7 +111,6 @@ export default class ThemeViewState implements SpecialVisualizationState {
readonly perLayerFiltered: ReadonlyMap<string, FilteringFeatureSource>
readonly availableLayers: Store<RasterLayerPolygon[]>
readonly selectedLayer: UIEventSource<LayerConfig>
readonly userRelatedState: UserRelatedState
readonly geolocation: GeoLocationHandler
readonly geolocationControl: GeolocationControlState
@ -171,7 +169,6 @@ export default class ThemeViewState implements SpecialVisualizationState {
})
this.userRelatedState = new UserRelatedState(
this.osmConnection,
layout?.language,
layout,
this.featureSwitches,
this.mapProperties
@ -180,18 +177,6 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.mapProperties.allowRotating.setData(fixated !== "yes")
})
this.selectedElement = new UIEventSource<Feature | undefined>(undefined, "Selected element")
this.selectedLayer = new UIEventSource<LayerConfig>(undefined, "Selected layer")
this.selectedElementAndLayer = this.selectedElement.mapD(
(feature) => {
const layer = this.selectedLayer.data
if (!layer) {
return undefined
}
return { layer, feature }
},
[this.selectedLayer]
)
this.geolocation = new GeoLocationHandler(
geolocationState,
@ -435,7 +420,6 @@ export default class ThemeViewState implements SpecialVisualizationState {
doShowLayer,
metaTags: this.userRelatedState.preferencesAsTags,
selectedElement: this.selectedElement,
selectedLayer: this.selectedLayer,
fetchStore: (id) => this.featureProperties.getStore(id),
})
})
@ -443,14 +427,12 @@ export default class ThemeViewState implements SpecialVisualizationState {
}
public openNewDialog() {
this.selectedLayer.setData(undefined)
this.selectedElement.setData(undefined)
const { lon, lat } = this.mapProperties.location.data
const feature = this.lastClickObject.createFeature(lon, lat)
this.featureProperties.trackFeature(feature)
this.selectedElement.setData(feature)
this.selectedLayer.setData(this.newPointDialog.layerDef)
}
/**
@ -515,14 +497,12 @@ export default class ThemeViewState implements SpecialVisualizationState {
}
const layer = this.layout.getMatchingLayer(toSelect.properties)
this.selectedElement.setData(undefined)
this.selectedLayer.setData(layer)
this.selectedElement.setData(toSelect)
})
return
}
const layer = this.layout.getMatchingLayer(toSelect.properties)
this.selectedElement.setData(undefined)
this.selectedLayer.setData(layer)
this.selectedElement.setData(toSelect)
}
@ -752,7 +732,6 @@ export default class ThemeViewState implements SpecialVisualizationState {
layer: flayer.layerDef,
metaTags: this.userRelatedState.preferencesAsTags,
selectedElement: this.selectedElement,
selectedLayer: this.selectedLayer,
})
})
}
@ -766,7 +745,6 @@ export default class ThemeViewState implements SpecialVisualizationState {
console.trace("Unselected")
// We did _unselect_ an item - we always remove the lastclick-object
this.lastClickObject.features.setData([])
this.selectedLayer.setData(undefined)
this.focusOnMap()
}
})

View file

@ -77,6 +77,9 @@ export class Orientation {
}
private update(event: DeviceOrientationEvent) {
if (event.alpha === null || event.beta === null || event.gamma === null) {
return
}
this.gotMeasurement.setData(true)
// IF the phone is lying flat, then:
// alpha is the compass direction (but not absolute)

View file

@ -56,7 +56,11 @@
.filter((key) => key.startsWith(prefix))
.map((key) => key.substring(prefix.length, key.length - "-enabled".length))
)
return hiddenThemes.filter((theme) => knownIds.has(theme.id) || state.osmConnection.userDetails.data.name === "Pieter Vander Vennet")
return hiddenThemes.filter(
(theme) =>
knownIds.has(theme.id) ||
state.osmConnection.userDetails.data.name === "Pieter Vander Vennet"
)
})
}
</script>

View file

@ -6,10 +6,14 @@
*/
export let selected: UIEventSource<boolean>
let _c: boolean = selected.data ?? true
let id = `checkbox-input-${Math.round(Math.random()*100000000)}`
$: selected.set(_c)
selected.addCallbackD(s => {
_c = s
})
</script>
<label class="no-image-background flex items-center gap-1">
<input bind:checked={_c} type="checkbox" />
<input bind:checked={_c} type="checkbox" {id} />
<slot />
</label>

View file

@ -39,9 +39,9 @@
let relativeDirections = Translations.t.general.visualFeedback.directionsRelative
let absoluteDirections = Translations.t.general.visualFeedback.directionsAbsolute
function round10(n :number){
if(n < 50){
function round10(n: number) {
if (n < 50) {
return n
}
return Math.round(n / 10) * 10
@ -132,7 +132,10 @@
so we use a 'div' and add on:click manually
-->
<div
class={twMerge("soft relative flex justify-center items-center border border-black rounded-full cursor-pointer p-1", size)}
class={twMerge(
"soft relative flex cursor-pointer items-center justify-center rounded-full border border-black p-1",
size
)}
on:click={() => focusMap()}
use:ariaLabelStore={label}
>
@ -140,18 +143,18 @@
</div>
{:else}
<div
class={twMerge("soft relative rounded-full border-black border", size)}
class={twMerge("soft relative rounded-full border border-black", size)}
on:click={() => focusMap()}
use:ariaLabelStore={label}
>
<div
class={twMerge(
"absolute top-0 left-0 flex items-center justify-center break-words text-xs cursor-pointer",
"absolute top-0 left-0 flex cursor-pointer items-center justify-center break-words text-xs",
size
)}
>
<div aria-hidden="true">
{GeoOperations.distanceToHuman($bearingAndDistGps?.dist)}
{GeoOperations.distanceToHuman($bearingAndDistGps?.dist)}
</div>
<div class="offscreen">
{$label}
@ -170,11 +173,11 @@
<style>
.offscreen {
clip: rect(1px, 1px, 1px, 1px);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap; /* added line */
width: 1px;
clip: rect(1px, 1px, 1px, 1px);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap; /* added line */
width: 1px;
}
</style>

View file

@ -16,19 +16,22 @@
if (e.target["id"] == id) {
return
}
if(formElement.contains(e.target) || document.getElementsByClassName("selected-element-view")[0]?.contains(e.target)){
if (
formElement.contains(e.target) ||
document.getElementsByClassName("selected-element-view")[0]?.contains(e.target)
) {
e.preventDefault()
if(e.type === "drop"){
if (e.type === "drop") {
console.log("Got a 'drop'", e)
drawAttention = false
dispatcher("submit", e.dataTransfer.files)
return
}
drawAttention = true
e.dataTransfer.dropEffect = "copy"
return
/*
drawAttention = false
@ -50,7 +53,6 @@
window.removeEventListener("dragover", handleDragEvent)
window.removeEventListener("drop", handleDragEvent)
})
</script>
<form

View file

@ -28,7 +28,7 @@
style="z-index: 21"
use:trapFocus
>
<div class="h-full content normal-background" on:click|stopPropagation={() => {}}>
<div class="content normal-background h-full" on:click|stopPropagation={() => {}}>
<div class="h-full rounded-xl">
<slot />
</div>

View file

@ -9,7 +9,7 @@
</script>
<button class={clss} on:click={() => osmConnection.AttemptLogin()} style="margin-left: 0">
<Login class="w-12 m-1" />
<Login class="m-1 w-12" />
<slot>
<Tr t={Translations.t.general.loginWithOpenStreetMap} />
</slot>

View file

@ -8,7 +8,7 @@
onMount(() => {
const uiElem = typeof construct === "function" ? construct() : construct
html = uiElem?.ConstructElement()
if (html !== undefined) {
elem?.replaceWith(html)
}

View file

@ -1,17 +1,49 @@
<script lang="ts">
/**
* THe panel containing all filter- and layerselection options
*/
/**
* THe panel containing all filter- and layerselection options
*/
import OverlayToggle from "./OverlayToggle.svelte"
import Filterview from "./Filterview.svelte"
import ThemeViewState from "../../Models/ThemeViewState"
import Translations from "../i18n/Translations"
import Tr from "../Base/Tr.svelte"
import Filter from "../../assets/svg/Filter.svelte"
import OverlayToggle from "./OverlayToggle.svelte"
import Filterview from "./Filterview.svelte"
import ThemeViewState from "../../Models/ThemeViewState"
import Translations from "../i18n/Translations"
import Tr from "../Base/Tr.svelte"
import Filter from "../../assets/svg/Filter.svelte"
export let state: ThemeViewState
let layout = state.layout
export let state: ThemeViewState
let layout = state.layout
let allEnabled : boolean
let allDisabled: boolean
function updateEnableState(){
allEnabled = true
allDisabled = true
state.layerState.filteredLayers.forEach((v) => {
if(!v.layerDef.name){
return
}
allEnabled &&= v.isDisplayed.data
allDisabled &&= !v.isDisplayed.data
})
}
updateEnableState()
state.layerState.filteredLayers.forEach((v) => {
if(!v.layerDef.name){
return
}
v.isDisplayed.addCallbackD(_ => updateEnableState())
})
function enableAll(doEnable: boolean){
state.layerState.filteredLayers.forEach((v) => {
if(!v.layerDef.name){
return
}
v.isDisplayed.setData(doEnable)
})
}
</script>
<div class="m-2 flex flex-col">
@ -27,6 +59,15 @@
highlightedLayer={state.guistate.highlightedLayerInFilters}
/>
{/each}
<div class="flex self-end mt-1">
<button class="small" class:disabled={allEnabled} on:click={() => enableAll(true)}>
<Tr t={Translations.t.general.filterPanel.enableAll}/>
</button>
<button class="small" class:disabled={allDisabled} on:click={() => enableAll(false)}>
<Tr t={Translations.t.general.filterPanel.disableAll}/>
</button>
</div>
{#each layout.tileLayerSources as tilesource}
<OverlayToggle
layerproperties={tilesource}

View file

@ -1,120 +1,120 @@
<script lang="ts">
import type { SpecialVisualizationState } from "../SpecialVisualization"
import LocationInput from "../InputElement/Helpers/LocationInput.svelte"
import { UIEventSource } from "../../Logic/UIEventSource"
import { Tiles } from "../../Models/TileRange"
import { Map as MlMap } from "maplibre-gl"
import { BBox } from "../../Logic/BBox"
import type { MapProperties } from "../../Models/MapProperties"
import ShowDataLayer from "../Map/ShowDataLayer"
import type {
FeatureSource,
FeatureSourceForLayer,
} from "../../Logic/FeatureSource/FeatureSource"
import SnappingFeatureSource from "../../Logic/FeatureSource/Sources/SnappingFeatureSource"
import FeatureSourceMerger from "../../Logic/FeatureSource/Sources/FeatureSourceMerger"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { Utils } from "../../Utils"
import { createEventDispatcher } from "svelte"
import Move_arrows from "../../assets/svg/Move_arrows.svelte"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import LocationInput from "../InputElement/Helpers/LocationInput.svelte"
import { UIEventSource } from "../../Logic/UIEventSource"
import { Tiles } from "../../Models/TileRange"
import { Map as MlMap } from "maplibre-gl"
import { BBox } from "../../Logic/BBox"
import type { MapProperties } from "../../Models/MapProperties"
import ShowDataLayer from "../Map/ShowDataLayer"
import type {
FeatureSource,
FeatureSourceForLayer,
} from "../../Logic/FeatureSource/FeatureSource"
import SnappingFeatureSource from "../../Logic/FeatureSource/Sources/SnappingFeatureSource"
import FeatureSourceMerger from "../../Logic/FeatureSource/Sources/FeatureSourceMerger"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { Utils } from "../../Utils"
import { createEventDispatcher } from "svelte"
import Move_arrows from "../../assets/svg/Move_arrows.svelte"
/**
* An advanced location input, which has support to:
* - Show more layers
* - Snap to layers
*
* This one is mostly used to insert new points, including when importing
*/
export let state: SpecialVisualizationState
/**
* The start coordinate
*/
export let coordinate: { lon: number; lat: number }
/**
* An advanced location input, which has support to:
* - Show more layers
* - Snap to layers
*
* This one is mostly used to insert new points, including when importing
*/
export let state: SpecialVisualizationState
/**
* The start coordinate
*/
export let coordinate: { lon: number; lat: number }
/**
* The center of the map at all times
* If undefined at the beginning, 'coordinate' will be used
*/
export let value: UIEventSource<{ lon: number; lat: number }>
if (value.data === undefined) {
value.setData(coordinate)
/**
* The center of the map at all times
* If undefined at the beginning, 'coordinate' will be used
*/
export let value: UIEventSource<{ lon: number; lat: number }>
if (value.data === undefined) {
value.setData(coordinate)
}
if (coordinate === undefined) {
coordinate = value.data
}
export let snapToLayers: string[] | undefined
export let targetLayer: LayerConfig | undefined
export let maxSnapDistance: number = undefined
export let snappedTo: UIEventSource<string | undefined>
let preciseLocation: UIEventSource<{ lon: number; lat: number }> = new UIEventSource<{
lon: number
lat: number
}>(undefined)
const dispatch = createEventDispatcher<{ click: { lon: number; lat: number } }>()
const xyz = Tiles.embedded_tile(coordinate.lat, coordinate.lon, 16)
const map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined)
let initialMapProperties: Partial<MapProperties> & { location } = {
zoom: new UIEventSource<number>(19),
maxbounds: new UIEventSource(undefined),
/*If no snapping needed: the value is simply the map location;
* If snapping is needed: the value will be set later on by the snapping feature source
* */
location:
snapToLayers?.length > 0
? new UIEventSource<{ lon: number; lat: number }>(coordinate)
: value,
bounds: new UIEventSource<BBox>(undefined),
allowMoving: new UIEventSource<boolean>(true),
allowZooming: new UIEventSource<boolean>(true),
minzoom: new UIEventSource<number>(18),
rasterLayer: UIEventSource.feedFrom(state.mapProperties.rasterLayer),
}
if (targetLayer) {
const featuresForLayer = state.perLayer.get(targetLayer.id)
if (featuresForLayer) {
new ShowDataLayer(map, {
layer: targetLayer,
features: featuresForLayer,
})
}
if (coordinate === undefined) {
coordinate = value.data
}
if (snapToLayers?.length > 0) {
const snapSources: FeatureSource[] = []
for (const layerId of snapToLayers ?? []) {
const layer: FeatureSourceForLayer = state.perLayer.get(layerId)
snapSources.push(layer)
if (layer.features === undefined) {
continue
}
new ShowDataLayer(map, {
layer: layer.layer.layerDef,
zoomToFeatures: false,
features: layer,
})
}
export let snapToLayers: string[] | undefined
export let targetLayer: LayerConfig | undefined
export let maxSnapDistance: number = undefined
const snappedLocation = new SnappingFeatureSource(
new FeatureSourceMerger(...Utils.NoNull(snapSources)),
// We snap to the (constantly updating) map location
initialMapProperties.location,
{
maxDistance: maxSnapDistance ?? 15,
allowUnsnapped: true,
snappedTo,
snapLocation: value,
}
)
export let snappedTo: UIEventSource<string | undefined>
let preciseLocation: UIEventSource<{ lon: number; lat: number }> = new UIEventSource<{
lon: number
lat: number
}>(undefined)
const dispatch = createEventDispatcher<{ click: { lon: number; lat: number } }>()
const xyz = Tiles.embedded_tile(coordinate.lat, coordinate.lon, 16)
const map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined)
let initialMapProperties: Partial<MapProperties> & {location} = {
zoom: new UIEventSource<number>(19),
maxbounds: new UIEventSource(undefined),
/*If no snapping needed: the value is simply the map location;
* If snapping is needed: the value will be set later on by the snapping feature source
* */
location:
snapToLayers?.length > 0
? new UIEventSource<{ lon: number; lat: number }>(coordinate)
: value,
bounds: new UIEventSource<BBox>(undefined),
allowMoving: new UIEventSource<boolean>(true),
allowZooming: new UIEventSource<boolean>(true),
minzoom: new UIEventSource<number>(18),
rasterLayer: UIEventSource.feedFrom(state.mapProperties.rasterLayer),
}
if (targetLayer) {
const featuresForLayer = state.perLayer.get(targetLayer.id)
if (featuresForLayer) {
new ShowDataLayer(map, {
layer: targetLayer,
features: featuresForLayer,
})
}
}
if (snapToLayers?.length > 0) {
const snapSources: FeatureSource[] = []
for (const layerId of snapToLayers ?? []) {
const layer: FeatureSourceForLayer = state.perLayer.get(layerId)
snapSources.push(layer)
if (layer.features === undefined) {
continue
}
new ShowDataLayer(map, {
layer: layer.layer.layerDef,
zoomToFeatures: false,
features: layer,
})
}
const snappedLocation = new SnappingFeatureSource(
new FeatureSourceMerger(...Utils.NoNull(snapSources)),
// We snap to the (constantly updating) map location
initialMapProperties.location,
{
maxDistance: maxSnapDistance ?? 15,
allowUnsnapped: true,
snappedTo,
snapLocation: value,
}
)
new ShowDataLayer(map, {
layer: targetLayer,
features: snappedLocation,
})
}
new ShowDataLayer(map, {
layer: targetLayer,
features: snappedLocation,
})
}
</script>
<LocationInput

View file

@ -28,7 +28,7 @@
<h5>{t.noMatchingThemes.toString()}</h5>
<div class="flex justify-center">
<button on:click={() => search.setData("")}>
<Search_disable class="w-6 mr-2" />
<Search_disable class="mr-2 w-6" />
<Tr t={t.noSearch} />
</button>
</div>

View file

@ -20,7 +20,7 @@
let result = await Geocoding.reverse(
mapProperties.location.data,
mapProperties.zoom.data,
Locale.language.data,
Locale.language.data
)
let properties = result.features[0].properties
currentLocation = properties.display_name
@ -44,7 +44,7 @@
() => {
displayLocation()
},
[Translations.t.hotkeyDocumentation.shakePhone],
[Translations.t.hotkeyDocumentation.shakePhone]
)
Motion.singleton.startListening()

View file

@ -13,7 +13,7 @@
export let layer: LayerConfig
export let selectedElement: Feature
let tags: UIEventSource<Record<string, string>> = state.featureProperties.getStore(
selectedElement.properties.id,
selectedElement.properties.id
)
$: {
tags = state.featureProperties.getStore(selectedElement.properties.id)
@ -25,9 +25,9 @@
{#if $tags._deleted === "yes"}
<Tr t={Translations.t.delete.isDeleted} />
{:else}
<div class="low-interaction border-b-2 border-black px-3 drop-shadow-md flex">
<div class="h-fit overflow-auto w-full sm:p-2" style="max-height: 20vh;">
<div class="flex flex-col flex-grow w-full h-full ">
<div class="low-interaction flex border-b-2 border-black px-3 drop-shadow-md">
<div class="h-fit w-full overflow-auto sm:p-2" style="max-height: 20vh;">
<div class="flex h-full w-full flex-grow flex-col">
<!-- Title element and title icons-->
<h3 class="m-0">
<a href={`#${$tags.id}`}>
@ -39,7 +39,7 @@
class="no-weblate title-icons links-as-button mr-2 flex flex-row flex-wrap items-center gap-x-0.5 pt-0.5 sm:pt-1"
>
{#each layer.titleIcons as titleIconConfig}
{#if (titleIconConfig.condition?.matchesProperties($tags) ?? true) && (titleIconConfig.metacondition?.matchesProperties({ ...$metatags, ...$tags }) ?? true) && titleIconConfig.IsKnown($tags)}
{#if (titleIconConfig.condition?.matchesProperties($tags) ?? true) && (titleIconConfig.metacondition?.matchesProperties( { ...$metatags, ...$tags } ) ?? true) && titleIconConfig.IsKnown($tags)}
<div class={titleIconConfig.renderIconClass ?? "flex h-8 w-8 items-center"}>
<TagRenderingAnswer
config={titleIconConfig}
@ -59,17 +59,16 @@
<button
on:click={() => state.selectedElement.setData(undefined)}
use:ariaLabel={Translations.t.general.backToMap}
class="rounded-full border-none p-0 shrink-0 h-fit mt-2"
class="mt-2 h-fit shrink-0 rounded-full border-none p-0"
style="border: 0 !important; padding: 0 !important;"
>
<XCircleIcon aria-hidden={true} class="h-8 w-8" />
</button>
</div>
{/if}
<style>
:global(.title-icons a) {
display: block !important;
}
:global(.title-icons a) {
display: block !important;
}
</style>

View file

@ -17,6 +17,9 @@
export let tags: UIEventSource<Record<string, string>> = state.featureProperties.getStore(
selectedElement.properties.id
)
let stillMatches = tags.map(tags => !layer?.source?.osmTags || layer.source.osmTags?.matchesProperties(tags))
let _metatags: Record<string, string>
onDestroy(
@ -35,7 +38,11 @@
)
</script>
{#if $tags._deleted === "yes"}
{#if !$stillMatches}
<div class="alert" aria-live="assertive">
<Tr t={Translations.t.delete.isChanged}/>
</div>
{:else if $tags._deleted === "yes"}
<div aria-live="assertive">
<Tr t={Translations.t.delete.isDeleted} />
</div>
@ -43,7 +50,10 @@
<Tr t={Translations.t.general.returnToTheMap} />
</button>
{:else}
<div class="flex h-full flex-col gap-y-2 overflow-y-auto p-1 px-4 w-full selected-element-view" tabindex="-1">
<div
class="selected-element-view flex h-full w-full flex-col gap-y-2 overflow-y-auto p-1 px-4"
tabindex="-1"
>
{#each $knownTagRenderings as config (config.id)}
<TagRenderingEditable
{tags}

View file

@ -74,7 +74,7 @@
<div class="flex">
{#if typeof navigator?.share === "function"}
<button class="h-8 w-8 shrink-0 p-1" on:click={shareCurrentLink}>
<Share/>
<Share />
</button>
{/if}
{#if navigator.clipboard !== undefined}

View file

@ -92,7 +92,7 @@
<Tr t={title} />
{#if selected}
<span class="alert" aria-hidden="true">
<span class="thanks hidden-on-mobile" aria-hidden="true">
<Tr t={Translations.t.general.morescreen.enterToOpen} />
</span>
{/if}

View file

@ -25,9 +25,10 @@
whenUploaded?: () => void | Promise<void>
} = undefined
let t = Translations.t.general.uploadGpx
let currentStep = new UIEventSource<"init" | "please_confirm" | "uploading" | "done" | "error">("init")
let currentStep = new UIEventSource<"init" | "please_confirm" | "uploading" | "done" | "error">(
"init"
)
let traceVisibilities: {
key: "private" | "public"
@ -44,8 +45,9 @@
},
]
let gpxServerIsOnline: Store<boolean> = state.osmConnection.gpxServiceIsOnline.map((serviceState) => serviceState === "online")
let gpxServerIsOnline: Store<boolean> = state.osmConnection.gpxServiceIsOnline.map(
(serviceState) => serviceState === "online"
)
/**
* More or less the same as the coalescing-operator '??', except that it checks for empty strings too
@ -63,18 +65,17 @@
let title: string = undefined
let description: string = undefined
let visibility = <UIEventSource<"public" | "private">>state?.osmConnection?.GetPreference("gps.trace.visibility") ?? new UIEventSource<"public" | "private">("private")
let visibility =
<UIEventSource<"public" | "private">>(
state?.osmConnection?.GetPreference("gps.trace.visibility")
) ?? new UIEventSource<"public" | "private">("private")
async function uploadTrace() {
try {
currentStep.setData("uploading")
const titleStr = createDefault(
title,
"Track with mapcomplete",
)
const titleStr = createDefault(title, "Track with mapcomplete")
const descriptionStr = createDefault(
description,
"Track created with MapComplete with theme " + state?.layout?.id,
"Track created with MapComplete with theme " + state?.layout?.id
)
await state?.osmConnection?.uploadGpxTrack(trace(titleStr), {
visibility: visibility.data ?? "private",
@ -95,47 +96,58 @@
</script>
<LoginToggle {state}>
{#if !$gpxServerIsOnline}
<div class="flex border alert items-center">
<Invalid class="w-8 h-8 m-2" />
<div class="alert flex items-center border">
<Invalid class="m-2 h-8 w-8" />
<Tr t={t.gpxServiceOffline} cls="p-2" />
</div>
{:else if $currentStep === "error"}
<div class="alert flex w-full gap-x-2">
<Invalid class="w-8 h-8"/>
<Invalid class="h-8 w-8" />
<Tr t={Translations.t.general.error} />
</div>
{:else if $currentStep === "init"}
<button class="flex w-full m-0" on:click={() => {currentStep.setData("please_confirm")}}>
<Upload class="w-12 h-12" />
<button
class="m-0 flex w-full"
on:click={() => {
currentStep.setData("please_confirm")
}}
>
<Upload class="h-12 w-12" />
<Tr t={t.title} />
</button>
{:else if $currentStep === "please_confirm"}
<form on:submit|preventDefault={() => uploadTrace()}
class="flex flex-col border-interactive interactive px-2 gap-y-1">
<form
on:submit|preventDefault={() => uploadTrace()}
class="border-interactive interactive flex flex-col gap-y-1 px-2"
>
<h2>
<Tr t={t.title} />
</h2>
<Tr t={t.intro0} />
<Tr t={t.intro1} />
<h3>
<Tr t={t.meta.title} />
</h3>
<Tr t={t.meta.intro} />
<input type="text" use:ariaLabel={t.meta.titlePlaceholder} use:placeholder={t.meta.titlePlaceholder}
bind:value={title} />
<input
type="text"
use:ariaLabel={t.meta.titlePlaceholder}
use:placeholder={t.meta.titlePlaceholder}
bind:value={title}
/>
<Tr t={t.meta.descriptionIntro} />
<textarea use:ariaLabel={t.meta.descriptionPlaceHolder} use:placeholder={t.meta.descriptionPlaceHolder}
bind:value={description} />
<textarea
use:ariaLabel={t.meta.descriptionPlaceHolder}
use:placeholder={t.meta.descriptionPlaceHolder}
bind:value={description}
/>
<Tr t={t.choosePermission} />
{#each traceVisibilities as option}
<label>
<input
type="radio"
@ -151,30 +163,25 @@
</label>
{/each}
<div class="flex flex-wrap-reverse justify-between items-stretch">
<button class="flex gap-x-2 w-1/2 flex-grow" on:click={() => currentStep.setData("init")}>
<Close class="w-8 h-8" />
<div class="flex flex-wrap-reverse items-stretch justify-between">
<button class="flex w-1/2 flex-grow gap-x-2" on:click={() => currentStep.setData("init")}>
<Close class="h-8 w-8" />
<Tr t={Translations.t.general.cancel} />
</button>
<button class="flex gap-x-2 primary flex-grow" on:click={() => uploadTrace()}>
<Upload class="w-8 h-8" />
<button class="primary flex flex-grow gap-x-2" on:click={() => uploadTrace()}>
<Upload class="h-8 w-8" />
<Tr t={t.confirm} />
</button>
</div>
</form>
{:else if $currentStep === "uploading"}
<Loading>
<Tr t={t.uploading} />
</Loading>
{:else if $currentStep === "done"}
<div class="flex p-2 rounded-xl border-2 subtle-border items-center">
<Confirm class="w-12 h-12 mr-2" />
<div class="subtle-border flex items-center rounded-xl border-2 p-2">
<Confirm class="mr-2 h-12 w-12" />
<Tr t={t.uploadFinished} />
</div>
{/if}

View file

@ -1,42 +1,43 @@
<script lang="ts">
import { UIEventSource } 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 ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"
import { Tag } from "../../Logic/Tags/Tag"
import Loading from "../Base/Loading.svelte"
import { UIEventSource } 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 ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"
import { Tag } from "../../Logic/Tags/Tag"
import Loading from "../Base/Loading.svelte"
export let key: string
export let externalProperties: Record<string, string>
export let key: string
export let externalProperties: Record<string, string>
export let tags: UIEventSource<OsmTags>
export let state: SpecialVisualizationState
export let feature: Feature
export let layer: LayerConfig
export let tags: UIEventSource<OsmTags>
export let state: SpecialVisualizationState
export let feature: Feature
export let layer: LayerConfig
export let readonly = false
export let readonly = false
let currentStep: "init" | "applying" | "done" = "init"
let currentStep: "init" | "applying" | "done" = "init"
/**
* Copy the given key into OSM
* @param key
*/
async function apply(key: string) {
currentStep = "applying"
const change = new ChangeTagAction(
tags.data.id,
new Tag(key, externalProperties[key]),
tags.data,
{
theme: state.layout.id,
changeType: "import",
})
await state.changes.applyChanges(await change.CreateChangeDescriptions())
currentStep = "done"
}
/**
* Copy the given key into OSM
* @param key
*/
async function apply(key: string) {
currentStep = "applying"
const change = new ChangeTagAction(
tags.data.id,
new Tag(key, externalProperties[key]),
tags.data,
{
theme: state.layout.id,
changeType: "import",
}
)
await state.changes.applyChanges(await change.CreateChangeDescriptions())
currentStep = "done"
}
</script>
<tr>
@ -54,14 +55,12 @@
{#if !readonly}
<td>
{#if currentStep === "init"}
<button class="small" on:click={() => apply(key)}>
Apply
</button>
<button class="small" on:click={() => apply(key)}>Apply</button>
{:else if currentStep === "applying"}
<Loading />
{:else if currentStep === "done"}
<div class="thanks">Done</div>
{:else }
{:else}
<div class="alert">Error</div>
{/if}
</td>

View file

@ -1,63 +1,62 @@
<script lang="ts">
import LinkableImage from "../Image/LinkableImage.svelte"
import { UIEventSource } 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 ComparisonAction from "./ComparisonAction.svelte"
import Party from "../../assets/svg/Party.svelte"
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"
import { Tag } from "../../Logic/Tags/Tag"
import { And } from "../../Logic/Tags/And"
import Loading from "../Base/Loading.svelte"
import AttributedImage from "../Image/AttributedImage.svelte"
import LinkableImage from "../Image/LinkableImage.svelte"
import { UIEventSource } 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 ComparisonAction from "./ComparisonAction.svelte"
import Party from "../../assets/svg/Party.svelte"
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"
import { Tag } from "../../Logic/Tags/Tag"
import { And } from "../../Logic/Tags/And"
import Loading from "../Base/Loading.svelte"
import AttributedImage from "../Image/AttributedImage.svelte"
export let osmProperties: Record<string, string>
export let externalProperties: Record<string, string>
export let osmProperties: Record<string, string>
export let externalProperties: Record<string, string>
export let tags: UIEventSource<OsmTags>
export let state: SpecialVisualizationState
export let feature: Feature
export let layer: LayerConfig
export let tags: UIEventSource<OsmTags>
export let state: SpecialVisualizationState
export let feature: Feature
export let layer: LayerConfig
export let readonly = false
export let readonly = false
let externalKeys: string[] = Object.keys(externalProperties).sort()
let externalKeys: string[] = (Object.keys(externalProperties))
.sort()
const imageKeyRegex = /image|image:[0-9]+/
console.log("Calculating knwon images")
let knownImages = new Set(
Object.keys(osmProperties)
.filter((k) => k.match(imageKeyRegex))
.map((k) => osmProperties[k])
)
console.log("Known images are:", knownImages)
let unknownImages = externalKeys
.filter((k) => k.match(imageKeyRegex))
.map((k) => externalProperties[k])
.filter((i) => !knownImages.has(i))
const imageKeyRegex = /image|image:[0-9]+/
console.log("Calculating knwon images")
let knownImages = new Set(Object.keys(osmProperties).filter(k => k.match(imageKeyRegex))
.map(k => osmProperties[k]))
console.log("Known images are:", knownImages)
let unknownImages = externalKeys.filter(k => k.match(imageKeyRegex))
.map(k => externalProperties[k])
.filter(i => !knownImages.has(i))
let propertyKeysExternal = externalKeys.filter((k) => k.match(imageKeyRegex) === null)
let missing = propertyKeysExternal.filter((k) => osmProperties[k] === undefined)
let same = propertyKeysExternal.filter((key) => osmProperties[key] === externalProperties[key])
let different = propertyKeysExternal.filter(
(key) => osmProperties[key] !== undefined && osmProperties[key] !== externalProperties[key]
)
let propertyKeysExternal = externalKeys.filter(k => k.match(imageKeyRegex) === null)
let missing = propertyKeysExternal.filter(k => osmProperties[k] === undefined)
let same = propertyKeysExternal.filter(key => osmProperties[key] === externalProperties[key])
let different = propertyKeysExternal.filter(key => osmProperties[key] !== undefined && osmProperties[key] !== externalProperties[key])
let currentStep: "init" | "applying_all" | "all_applied" = "init"
async function applyAllMissing() {
currentStep = "applying_all"
const tagsToApply = missing.map(k => new Tag(k, externalProperties[k]))
const change = new ChangeTagAction(
tags.data.id,
new And(tagsToApply),
tags.data,
{
theme: state.layout.id,
changeType: "import",
})
await state.changes.applyChanges(await change.CreateChangeDescriptions())
currentStep = "all_applied"
}
let currentStep: "init" | "applying_all" | "all_applied" = "init"
async function applyAllMissing() {
currentStep = "applying_all"
const tagsToApply = missing.map((k) => new Tag(k, externalProperties[k]))
const change = new ChangeTagAction(tags.data.id, new And(tagsToApply), tags.data, {
theme: state.layout.id,
changeType: "import",
})
await state.changes.applyChanges(await change.CreateChangeDescriptions())
currentStep = "all_applied"
}
</script>
{#if different.length > 0}
@ -89,7 +88,6 @@
{#each missing as key}
<ComparisonAction {key} {state} {tags} {externalProperties} {layer} {feature} {readonly} />
{/each}
</table>
{#if !readonly}
<button on:click={() => applyAllMissing()}>Apply all missing values</button>
@ -97,16 +95,13 @@
{:else if currentStep === "applying_all"}
<Loading>Applying all missing values</Loading>
{:else if currentStep === "all_applied"}
<div class="thanks">
All values are applied
</div>
<div class="thanks">All values are applied</div>
{/if}
{/if}
{#if unknownImages.length === 0 && missing.length === 0 && different.length === 0}
<div class="thanks flex items-center gap-x-2 px-2 m-0">
<Party class="w-8 h-8" />
<div class="thanks m-0 flex items-center gap-x-2 px-2">
<Party class="h-8 w-8" />
All data from Velopark is also included into OpenStreetMap
</div>
{/if}
@ -114,31 +109,32 @@
{#if unknownImages.length > 0}
{#if readonly}
<div class="w-full overflow-x-auto">
<div class="flex w-max gap-x-2 h-32">
<div class="flex h-32 w-max gap-x-2">
{#each unknownImages as image}
<AttributedImage imgClass="h-32 w-max shrink-0" image={{url:image}} previewedImage={state.previewedImage}/>
<AttributedImage
imgClass="h-32 w-max shrink-0"
image={{ url: image }}
previewedImage={state.previewedImage}
/>
{/each}
</div>
</div>
{:else}
{#each unknownImages as image}
<LinkableImage
{tags}
{state}
image={{
pictureUrl: image,
provider: "Velopark",
thumbUrl: image,
details: undefined,
coordinates: undefined,
osmTags : {image}
} }
pictureUrl: image,
provider: "Velopark",
thumbUrl: image,
details: undefined,
coordinates: undefined,
osmTags: { image },
}}
{feature}
{layer} />
{layer}
/>
{/each}
{/if}
{/if}

View file

@ -1,55 +1,54 @@
<script lang="ts">/**
* The comparison tool loads json-data from a speficied URL, eventually post-processes it
* and compares it with the current object
*/
import { onMount } from "svelte"
import { Utils } from "../../Utils"
import VeloparkLoader from "../../Logic/Web/VeloparkLoader"
import Loading from "../Base/Loading.svelte"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import { UIEventSource } from "../../Logic/UIEventSource"
import ComparisonTable from "./ComparisonTable.svelte"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import type { Feature } from "geojson"
import type { OsmTags } from "../../Models/OsmFeature"
<script lang="ts">
/**
* The comparison tool loads json-data from a speficied URL, eventually post-processes it
* and compares it with the current object
*/
import { onMount } from "svelte"
import { Utils } from "../../Utils"
import VeloparkLoader from "../../Logic/Web/VeloparkLoader"
import Loading from "../Base/Loading.svelte"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import { UIEventSource } from "../../Logic/UIEventSource"
import ComparisonTable from "./ComparisonTable.svelte"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import type { Feature } from "geojson"
import type { OsmTags } from "../../Models/OsmFeature"
export let url: string
export let postprocessVelopark: boolean
export let state: SpecialVisualizationState
export let tags: UIEventSource<OsmTags>
export let layer: LayerConfig
export let feature: Feature
export let readonly = false
let data: any = undefined
let error: any = undefined
export let url: string
export let postprocessVelopark: boolean
export let state: SpecialVisualizationState
export let tags: UIEventSource<OsmTags>
export let layer: LayerConfig
export let feature: Feature
export let readonly = false
let data: any = undefined
let error: any = undefined
onMount(async () => {
onMount(async () => {
const _url = tags.data[url]
if (!_url) {
error = "No URL found in attribute" + url
error = "No URL found in attribute" + url
}
try {
console.log("Attempting to download", _url)
const downloaded = await Utils.downloadJsonAdvanced(_url)
if (downloaded["error"]) {
console.error(downloaded)
error = downloaded["error"]
return
}
if (postprocessVelopark) {
data = VeloparkLoader.convert(downloaded["content"])
return
}
data = downloaded["content"]
console.log("Attempting to download", _url)
const downloaded = await Utils.downloadJsonAdvanced(_url)
if (downloaded["error"]) {
console.error(downloaded)
error = downloaded["error"]
return
}
if (postprocessVelopark) {
data = VeloparkLoader.convert(downloaded["content"])
return
}
data = downloaded["content"]
} catch (e) {
console.error(e)
error = "" + e
console.error(e)
error = "" + e
}
})
})
</script>
{#if error !== undefined}
<div class="alert">
Something went wrong: {error}
@ -59,5 +58,13 @@ onMount(async () => {
Loading {$tags[url]}
</Loading>
{:else if data.properties !== undefined}
<ComparisonTable externalProperties={data.properties} osmProperties={$tags} {state} {feature} {layer} {tags} {readonly} />
<ComparisonTable
externalProperties={data.properties}
osmProperties={$tags}
{state}
{feature}
{layer}
{tags}
{readonly}
/>
{/if}

View file

@ -29,7 +29,7 @@
center()
}
const titleIconBlacklist = ["osmlink", "sharelink", "favourite_title_icon"]
let titleIconBlacklist = ["osmlink", "sharelink", "favourite_title_icon"]
</script>
{#if favLayer !== undefined}
@ -51,7 +51,7 @@
class="title-icons links-as-button flex flex-wrap items-center gap-x-0.5 self-end justify-self-end p-1 pt-0.5 sm:pt-1"
>
{#each favConfig.titleIcons as titleIconConfig}
{#if titleIconBlacklist.indexOf(titleIconConfig.id) < 0 && (titleIconConfig.condition?.matchesProperties(properties) ?? true) && (titleIconConfig.metacondition?.matchesProperties({ ...properties, ...state.userRelatedState.preferencesAsTags.data }) ?? true) && titleIconConfig.IsKnown(properties)}
{#if titleIconBlacklist.indexOf(titleIconConfig.id) < 0 && (titleIconConfig.condition?.matchesProperties(properties) ?? true) && (titleIconConfig.metacondition?.matchesProperties( { ...properties, ...state.userRelatedState.preferencesAsTags.data } ) ?? true) && titleIconConfig.IsKnown(properties)}
<div class={titleIconConfig.renderIconClass ?? "flex h-8 w-8 items-center"}>
<TagRenderingAnswer
config={titleIconConfig}

View file

@ -23,7 +23,7 @@
"mapcomplete-favourites-" + new Date().toISOString() + ".geojson",
{
mimetype: "application/vnd.geo+json",
},
}
)
}
@ -34,7 +34,7 @@
"mapcomplete-favourites-" + new Date().toISOString() + ".gpx",
{
mimetype: "{gpx=application/gpx+xml}",
},
}
)
}
</script>
@ -48,7 +48,7 @@
<div class="flex flex-col" on:keypress={(e) => console.log("Got keypress", e)}>
<Tr t={Translations.t.favouritePoi.intro.Subs({ length: $favourites?.length ?? 0 })} />
<Tr t={Translations.t.favouritePoi.priintroPrivacyvacy} />
<Tr t={Translations.t.favouritePoi.introPrivacy} />
{#each $favourites as feature (feature.properties.id)}
<FavouriteSummary {feature} {state} />

View file

@ -33,7 +33,7 @@
id: Object.values(image.osmTags)[0],
}
function applyLink(isLinked :boolean) {
function applyLink(isLinked: boolean) {
console.log("Applying linked image", isLinked, targetValue)
const currentTags = tags.data
const key = Object.keys(image.osmTags)[0]
@ -57,7 +57,7 @@
}
}
}
isLinked.addCallback(isLinked => applyLink(isLinked))
isLinked.addCallback((isLinked) => applyLink(isLinked))
</script>
<div class="flex w-fit shrink-0 flex-col">

View file

@ -23,7 +23,7 @@
function update() {
const v = currentVal.data
const l = currentLang.data
if (<any> translations.data === "" || translations.data === undefined) {
if (<any>translations.data === "" || translations.data === undefined) {
translations.data = {}
}
if (translations.data[l] === v) {

View file

@ -30,8 +30,7 @@
*/
export let unvalidatedText = new UIEventSource(value.data ?? "")
if(unvalidatedText == /*Compare by reference!*/ value){
if (unvalidatedText == /*Compare by reference!*/ value) {
throw "Value and unvalidatedText may not be the same store!"
}
let validator: Validator = Validators.get(type ?? "string")
@ -55,14 +54,12 @@
}
}
function onKeyPress(e: KeyboardEvent){
if(e.key === "Enter"){
e.stopPropagation()
e.preventDefault()
dispatch("submit")
}
function onKeyPress(e: KeyboardEvent) {
if (e.key === "Enter") {
e.stopPropagation()
e.preventDefault()
dispatch("submit")
}
}
initValueAndDenom()
@ -139,7 +136,7 @@
let htmlElem: HTMLInputElement | HTMLTextAreaElement
let dispatch = createEventDispatcher<{ selected, submit }>()
let dispatch = createEventDispatcher<{ selected; submit }>()
$: {
if (htmlElem !== undefined) {
htmlElem.onfocus = () => dispatch("selected")
@ -174,7 +171,13 @@
{/if}
{#if unit !== undefined}
<UnitInput {unit} {selectedUnit} textValue={unvalidatedText} upstreamValue={value} {getCountry} />
<UnitInput
{unit}
{selectedUnit}
textValue={unvalidatedText}
upstreamValue={value}
{getCountry}
/>
{/if}
</div>
{/if}

View file

@ -59,7 +59,7 @@ export default class Validators {
"fediverse",
"id",
"slope",
"velopark"
"velopark",
] as const
public static readonly AllValidators: ReadonlyArray<Validator> = [
@ -88,7 +88,7 @@ export default class Validators {
new FediverseValidator(),
new IdValidator(),
new SlopeValidator(),
new VeloparkValidator()
new VeloparkValidator(),
]
private static _byType = Validators._byTypeConstructor()

View file

@ -17,6 +17,7 @@ export default class FediverseValidator extends Validator {
* @param s
*/
reformat(s: string): string {
s = s.trim()
if (!s.startsWith("@")) {
s = "@" + s
}
@ -35,6 +36,7 @@ export default class FediverseValidator extends Validator {
return undefined
}
getFeedback(s: string): Translation | undefined {
s = s.trim()
const match = s.match(FediverseValidator.usernameAtServer)
console.log("Match:", match)
if (match) {

View file

@ -12,14 +12,22 @@ export default class VeloparkValidator extends UrlValidator {
return superF
}
const url = new URL(s)
if (url.hostname !== "velopark.be" && url.hostname !== "www.velopark.be" && url.hostname !== "data.velopark.be") {
if (
url.hostname !== "velopark.be" &&
url.hostname !== "www.velopark.be" &&
url.hostname !== "data.velopark.be"
) {
return new Translation({ "*": "Invalid hostname, expected velopark.be" })
}
if(!s.startsWith("https://data.velopark.be/data/") && !s.startsWith("https://www.velopark.be/static/data/")){
return new Translation({"*":"A valid URL should either start with https://data.velopark.be/data/ or https://www.velopark.be/static/data/"})
if (
!s.startsWith("https://data.velopark.be/data/") &&
!s.startsWith("https://www.velopark.be/static/data/")
) {
return new Translation({
"*": "A valid URL should either start with https://data.velopark.be/data/ or https://www.velopark.be/static/data/",
})
}
}
public isValid(str: string) {
@ -28,9 +36,9 @@ export default class VeloparkValidator extends UrlValidator {
reformat(str: string): string {
const url = new URL(super.reformat(str))
if(url.pathname.startsWith("/static/data/")){
const id = str.substring(str.lastIndexOf("/")+1)
return "https://data.velopark.be/data/"+id
if (url.pathname.startsWith("/static/data/")) {
const id = str.substring(str.lastIndexOf("/") + 1)
return "https://data.velopark.be/data/" + id
}
return super.reformat(str)
}

View file

@ -44,7 +44,6 @@
export let icon: string | undefined
export let color: string | undefined = undefined
export let clss: string | undefined = undefined
</script>
{#if icon}
@ -56,7 +55,6 @@
<Square_rounded {color} class={clss} />
{:else if icon === "bug"}
<Bug {color} class={clss} />
{:else if icon === "circle"}
<Circle {color} class={clss} />
{:else if icon === "checkmark"}
@ -108,7 +106,7 @@
{:else if icon === "invalid"}
<Invalid {color} class={clss} />
{:else if icon === "heart"}
<HeartIcon style="--svg-color: {color}" class={twMerge(clss,"apply-fill")} />
<HeartIcon style="--svg-color: {color}" class={twMerge(clss, "apply-fill")} />
{:else if icon === "heart_outline"}
<HeartOutlineIcon style="--svg-color: {color}" class={twMerge(clss, "apply-fill")} />
{:else if icon === "confirm"}
@ -129,4 +127,3 @@
<img class={clss ?? "h-full w-full"} src={icon} aria-hidden="true" alt="" />
{/if}
{/if}

View file

@ -90,7 +90,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
this.lastClickLocation = lastClickLocation
const self = this
const rasterLayerHandler = new RasterLayerHandler(this._maplibreMap, this.rasterLayer)
new RasterLayerHandler(this._maplibreMap, this.rasterLayer)
function handleClick(e) {
if (e.originalEvent["consumed"]) {
@ -114,7 +114,6 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
self.setMaxzoom(self.maxzoom.data)
self.setBounds(self.bounds.data)
self.setTerrain(self.useTerrain.data)
rasterLayerHandler.setBackground()
this.updateStores(true)
})
self.MoveMapToCurrentLoc(self.location.data)
@ -128,7 +127,6 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
self.setBounds(self.bounds.data)
self.SetRotation(self.rotation.data)
self.setTerrain(self.useTerrain.data)
rasterLayerHandler.setBackground()
this.updateStores(true)
map.on("moveend", () => this.updateStores())
map.on("click", (e) => {
@ -177,7 +175,6 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
})
})
this.rasterLayer.addCallbackAndRun((_) => rasterLayerHandler.setBackground())
this.location.addCallbackAndRunD((loc) => {
self.MoveMapToCurrentLoc(loc)
})
@ -193,7 +190,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
)
this.allowZooming.addCallbackAndRun((allowZooming) => self.setAllowZooming(allowZooming))
this.bounds.addCallbackAndRunD((bounds) => self.setBounds(bounds))
this.useTerrain?.addCallbackAndRun(useTerrain => self.setTerrain(useTerrain))
this.useTerrain?.addCallbackAndRun((useTerrain) => self.setTerrain(useTerrain))
}
/**
@ -599,24 +596,25 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
}
const id = "maptiler-terrain-data"
if (useTerrain) {
if(map.getTerrain()){
return
if (map.getTerrain()) {
return
}
map.addSource(id, {
"type": "raster-dem",
"url": "https://api.maptiler.com/tiles/terrain-rgb/tiles.json?key=" + Constants.maptilerApiKey
type: "raster-dem",
url:
"https://api.maptiler.com/tiles/terrain-rgb/tiles.json?key=" +
Constants.maptilerApiKey,
})
try{
try {
while (!map?.isStyleLoaded()) {
await Utils.waitFor(250)
}
map.setTerrain({
source: id
})
}catch (e) {
map.setTerrain({
source: id,
})
} catch (e) {
console.error(e)
}
}
}
}

View file

@ -11,10 +11,11 @@
import type { MapProperties } from "../../Models/MapProperties"
import { onDestroy } from "svelte"
import type { RasterLayerPolygon } from "../../Models/RasterLayers"
import StyleLoadingIndicator from "./StyleLoadingIndicator.svelte"
export let placedOverMapProperties: MapProperties
export let placedOverMap: Store<MlMap>
export let interactive: boolean = undefined
export let rasterLayer: UIEventSource<RasterLayerPolygon>
@ -25,7 +26,7 @@
rasterLayer,
zoom: UIEventSource.feedFrom(placedOverMapProperties.zoom),
rotation: UIEventSource.feedFrom(placedOverMapProperties.rotation),
pitch: UIEventSource.feedFrom(placedOverMapProperties.pitch)
pitch: UIEventSource.feedFrom(placedOverMapProperties.pitch),
})
altproperties.allowMoving.setData(false)
altproperties.allowZooming.setData(false)
@ -64,9 +65,13 @@
updateLocation()
window.setTimeout(updateLocation, 150)
window.setTimeout(updateLocation, 500)
})
}),
)
}
</script>
<div class="absolute w-full h-full flex items-center justify-center"
style="z-index: 100">
<StyleLoadingIndicator map={altmap} />
</div>
<MaplibreMap {interactive} map={altmap} />

View file

@ -71,7 +71,7 @@ class SingleBackgroundHandler {
this.fadeOut()
} else {
this._deactivationTime = undefined
this.enable()
await this.enable()
this.fadeIn()
}
}
@ -85,10 +85,23 @@ class SingleBackgroundHandler {
}
}
private enable() {
private async enable(){
let ttl = 15
await this.awaitStyleIsLoaded()
while(!this.tryEnable() && ttl > 0){
ttl --;
await Utils.waitFor(250)
}
}
/**
* Returns 'false' if should be attempted again
* @private
*/
private tryEnable(): boolean {
const map: MLMap = this._map.data
if (!map) {
return
return true
}
const background = this._targetLayer.properties
console.debug("Enabling", background.id)
@ -101,8 +114,7 @@ class SingleBackgroundHandler {
try {
map.addSource(background.id, RasterLayerHandler.prepareWmsSource(background))
} catch (e) {
console.error("Could not add source", e)
return
return false
}
}
if (!map.getLayer(background.id)) {
@ -126,6 +138,7 @@ class SingleBackgroundHandler {
map.setPaintProperty(background.id, "raster-opacity", o)
})
}
return true
}
private fadeOut() {
@ -144,23 +157,15 @@ class SingleBackgroundHandler {
}
export default class RasterLayerHandler {
private _map: Store<MLMap>
private _background: UIEventSource<RasterLayerPolygon | undefined>
private _singleLayerHandlers: Record<string, SingleBackgroundHandler> = {}
constructor(map: Store<MLMap>, background: UIEventSource<RasterLayerPolygon | undefined>) {
this._map = map
this._background = background
background.addCallbackAndRunD((l) => {
const key = l.properties.id
if (!this._singleLayerHandlers[key]) {
this._singleLayerHandlers[key] = new SingleBackgroundHandler(map, l, background)
}
})
map.addCallback((map) => {
map.on("load", () => this.setBackground())
this.setBackground()
})
}
public static prepareWmsSource(layer: RasterLayerProperties): SourceSpecification {
@ -203,9 +208,4 @@ export default class RasterLayerHandler {
return url
}
/**
* Performs all necessary updates
*/
public setBackground() {}
}

View file

@ -5,6 +5,7 @@
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { Map as MlMap } from "maplibre-gl"
import { createEventDispatcher, onDestroy } from "svelte"
import StyleLoadingIndicator from "./StyleLoadingIndicator.svelte"
/***
* Chooses a background-layer out of available options
@ -67,9 +68,9 @@
mapproperties.rasterLayer.setData(rasterLayer.data)
dispatch("appliedLayer")
}
function handleKeyPress(e: KeyboardEvent){
if(e.key === "Enter"){
function handleKeyPress(e: KeyboardEvent) {
if (e.key === "Enter") {
apply()
}
}
@ -77,21 +78,16 @@
{#if hasLayers}
<form class="flex h-full w-full flex-col" on:submit|preventDefault={() => {}}>
<button
tabindex="-1"
on:click={() => apply()}
class="m-0 h-full w-full p-1 cursor-pointer"
>
<div class="pointer-events-none w-full h-full">
<OverlayMap
interactive={false}
rasterLayer={rasterLayerOnMap}
placedOverMap={map}
placedOverMapProperties={mapproperties}
{visible}
/>
</div>
<button tabindex="-1" on:click={() => apply()} class="m-0 h-full w-full cursor-pointer p-1">
<span class="pointer-events-none h-full w-full relative">
<OverlayMap
interactive={false}
rasterLayer={rasterLayerOnMap}
placedOverMap={map}
placedOverMapProperties={mapproperties}
{visible}
/>
</span>
</button>
<select bind:value={$rasterLayer} class="w-full" on:keydown={handleKeyPress}>
{#each $availableLayers as availableLayer}

View file

@ -37,7 +37,7 @@ class PointRenderingLayer {
visibility?: Store<boolean>,
fetchStore?: (id: string) => Store<Record<string, string>>,
onClick?: (feature: Feature) => void,
selectedElement?: Store<{ properties: { id?: string } }>,
selectedElement?: Store<{ properties: { id?: string } }>
) {
this._visibility = visibility
this._config = config
@ -90,7 +90,7 @@ class PointRenderingLayer {
" while rendering",
location,
"of",
this._config,
this._config
)
}
const id = feature.properties.id + "-" + location
@ -98,7 +98,7 @@ class PointRenderingLayer {
const loc = GeoOperations.featureToCoordinateWithRenderingType(
<any>feature,
location,
location
)
if (loc === undefined) {
continue
@ -154,7 +154,7 @@ class PointRenderingLayer {
if (this._onClick) {
const self = this
el.addEventListener("click", function(ev) {
el.addEventListener("click", function (ev) {
ev.preventDefault()
self._onClick(feature)
// Workaround to signal the MapLibreAdaptor to ignore this click
@ -222,7 +222,7 @@ class LineRenderingLayer {
config: LineRenderingConfig,
visibility?: Store<boolean>,
fetchStore?: (id: string) => Store<Record<string, string>>,
onClick?: (feature: Feature) => void,
onClick?: (feature: Feature) => void
) {
this._layername = layername
this._map = map
@ -240,47 +240,49 @@ class LineRenderingLayer {
this._map.removeLayer(this._layername + "_polygon")
}
private async addSymbolLayer(sourceId: string, imageAlongWay: { if?: TagsFilter, then: string }[]) {
private async addSymbolLayer(
sourceId: string,
imageAlongWay: { if?: TagsFilter; then: string }[]
) {
const map = this._map
await Promise.allSettled(imageAlongWay.map(async (img, i) => {
const imgId = img.then.replaceAll(/[/.-]/g, "_")
if (map.getImage(imgId) === undefined) {
await new Promise<void>((resolve, reject) => {
map.loadImage(img.then, (err, image) => {
if (err) {
console.error("Could not add symbol layer to line due to", err)
return
}
map.addImage(imgId, image)
resolve()
await Promise.allSettled(
imageAlongWay.map(async (img, i) => {
const imgId = img.then.replaceAll(/[/.-]/g, "_")
if (map.getImage(imgId) === undefined) {
await new Promise<void>((resolve, reject) => {
map.loadImage(img.then, (err, image) => {
if (err) {
console.error("Could not add symbol layer to line due to", err)
return
}
map.addImage(imgId, image)
resolve()
})
})
})
}
const spec: AddLayerObject = {
"id": "symbol-layer_" + this._layername + "-" + i,
"type": "symbol",
"source": sourceId,
"layout": {
"symbol-placement": "line",
"symbol-spacing": 10,
"icon-allow-overlap": true,
"icon-rotation-alignment": "map",
"icon-pitch-alignment": "map",
"icon-image": imgId,
"icon-size": 0.055,
},
}
const filter = img.if?.asMapboxExpression()
console.log(">>>", this._layername, imgId, img.if, "-->", filter)
if (filter) {
spec.filter = filter
}
map.addLayer(spec)
}))
}
const spec: AddLayerObject = {
id: "symbol-layer_" + this._layername + "-" + i,
type: "symbol",
source: sourceId,
layout: {
"symbol-placement": "line",
"symbol-spacing": 10,
"icon-allow-overlap": true,
"icon-rotation-alignment": "map",
"icon-pitch-alignment": "map",
"icon-image": imgId,
"icon-size": 0.055,
},
}
const filter = img.if?.asMapboxExpression()
console.log(">>>", this._layername, imgId, img.if, "-->", filter)
if (filter) {
spec.filter = filter
}
map.addLayer(spec)
})
)
}
/**
@ -289,7 +291,7 @@ class LineRenderingLayer {
* @private
*/
private calculatePropsFor(
properties: Record<string, string>,
properties: Record<string, string>
): Partial<Record<(typeof LineRenderingLayer.lineConfigKeys)[number], string>> {
const config = this._config
@ -369,7 +371,6 @@ class LineRenderingLayer {
this.addSymbolLayer(this._layername, this._config.imageAlongWay)
}
for (const feature of features) {
if (!feature.properties.id) {
console.warn("Feature without id:", feature)
@ -377,7 +378,7 @@ class LineRenderingLayer {
}
map.setFeatureState(
{ source: this._layername, id: feature.properties.id },
this.calculatePropsFor(feature.properties),
this.calculatePropsFor(feature.properties)
)
}
@ -420,7 +421,7 @@ class LineRenderingLayer {
"Error while setting visibility of layers ",
linelayer,
polylayer,
e,
e
)
}
})
@ -441,7 +442,7 @@ class LineRenderingLayer {
console.trace(
"Got a feature without ID; this causes rendering bugs:",
feature,
"from",
"from"
)
LineRenderingLayer.missingIdTriggered = true
}
@ -453,7 +454,7 @@ class LineRenderingLayer {
if (this._fetchStore === undefined) {
map.setFeatureState(
{ source: this._layername, id },
this.calculatePropsFor(feature.properties),
this.calculatePropsFor(feature.properties)
)
} else {
const tags = this._fetchStore(id)
@ -470,7 +471,7 @@ class LineRenderingLayer {
}
map.setFeatureState(
{ source: this._layername, id },
this.calculatePropsFor(properties),
this.calculatePropsFor(properties)
)
})
}
@ -494,7 +495,7 @@ export default class ShowDataLayer {
layer: LayerConfig
drawMarkers?: true | boolean
drawLines?: true | boolean
},
}
) {
this._options = options
const self = this
@ -505,7 +506,7 @@ export default class ShowDataLayer {
mlmap: UIEventSource<MlMap>,
features: FeatureSource,
layers: LayerConfig[],
options?: Partial<ShowDataLayerOptions>,
options?: Partial<ShowDataLayerOptions>
) {
const perLayer: PerLayerFeatureSourceSplitter<FeatureSourceForLayer> =
new PerLayerFeatureSourceSplitter(
@ -513,7 +514,7 @@ export default class ShowDataLayer {
features,
{
constructStore: (features, layer) => new SimpleFeatureSource(layer, features),
},
}
)
perLayer.forEach((fs) => {
new ShowDataLayer(mlmap, {
@ -527,7 +528,7 @@ export default class ShowDataLayer {
public static showRange(
map: Store<MlMap>,
features: FeatureSource,
doShowLayer?: Store<boolean>,
doShowLayer?: Store<boolean>
): ShowDataLayer {
return new ShowDataLayer(map, {
layer: ShowDataLayer.rangeLayer,
@ -536,8 +537,7 @@ export default class ShowDataLayer {
})
}
public destruct() {
}
public destruct() {}
private zoomToCurrentFeatures(map: MlMap) {
if (this._options.zoomToFeatures) {
@ -558,9 +558,9 @@ export default class ShowDataLayer {
(this._options.layer.title === undefined
? undefined
: (feature: Feature) => {
selectedElement?.setData(feature)
selectedLayer?.setData(this._options.layer)
})
selectedElement?.setData(feature)
selectedLayer?.setData(this._options.layer)
})
if (this._options.drawLines !== false) {
for (let i = 0; i < this._options.layer.lineRendering.length; i++) {
const lineRenderingConfig = this._options.layer.lineRendering[i]
@ -571,7 +571,7 @@ export default class ShowDataLayer {
lineRenderingConfig,
doShowLayer,
fetchStore,
onClick,
onClick
)
this.onDestroy.push(l.destruct)
}
@ -586,7 +586,7 @@ export default class ShowDataLayer {
doShowLayer,
fetchStore,
onClick,
selectedElement,
selectedElement
)
}
}

View file

@ -14,11 +14,6 @@ export interface ShowDataLayerOptions {
*/
selectedElement?: UIEventSource<Feature>
/**
* When a feature of this layer is tapped, the layer will be marked
*/
selectedLayer?: UIEventSource<LayerConfig>
/**
* If set, zoom to the features when initially loaded and when they are changed
*/

View file

@ -0,0 +1,19 @@
<script lang="ts">
import Loading from "../Base/Loading.svelte"
import { Stores, UIEventSource } from "../../Logic/UIEventSource"
import { Map as MlMap } from "maplibre-gl"
import { onDestroy } from "svelte"
let isLoading = false
export let map: UIEventSource<MlMap>
onDestroy(Stores.Chronic(250).addCallback(
() => {
isLoading = !map.data?.isStyleLoaded()
},
))
</script>
{#if isLoading}
<Loading />
{/if}

View file

@ -6,6 +6,7 @@
import Translations from "../i18n/Translations"
import Icon from "../Map/Icon.svelte"
import Maproulette from "../../Logic/Maproulette"
import LoginToggle from "../Base/LoginToggle.svelte"
/**
* A UI-element to change the status of a maproulette-task
@ -18,8 +19,11 @@
export let statusToSet: string
export let maproulette_id_key: string
export let askFeedback: string = ""
let applying = false
let failed = false
let feedback: string = ""
/** Current status of the task*/
let status: Store<number> = tags
@ -36,6 +40,7 @@
try {
await Maproulette.singleton.closeTask(Number(maproulette_id), Number(statusToSet), {
tags: `MapComplete MapComplete:${state.layout.id}`,
comment: feedback,
})
tags.data["mr_taskStatus"] = Maproulette.STATUS_MEANING[Number(statusToSet)]
tags.data.status = statusToSet
@ -47,17 +52,35 @@
}
</script>
{#if failed}
<div class="alert">ERROR - could not close the MapRoulette task</div>
{:else if applying}
<Loading>
<Tr t={Translations.t.general.loading} />
</Loading>
{:else if $status === Maproulette.STATUS_OPEN}
<button class="no-image-background w-full p-4 m-0" on:click={() => apply()}>
<Icon clss="w-8 h-8 mr-2 shrink-0" icon={image} />
{message}
</button>
{:else}
{message_closed}
{/if}
<LoginToggle ignoreLoading={true} {state}>
{#if failed}
<div class="alert">ERROR - could not close the MapRoulette task</div>
{:else if applying}
<Loading>
<Tr t={Translations.t.general.loading} />
</Loading>
{:else if $status === Maproulette.STATUS_OPEN}
{#if askFeedback !== "" && askFeedback !== undefined}
<div class="interactive flex flex-col gap-y-1 border border-dashed border-gray-500 p-1">
<h3>{askFeedback}</h3>
<textarea bind:value={feedback} />
<button
class="no-image-background m-0 w-full p-4"
class:disabled={feedback === ""}
on:click={() => apply()}
>
<Icon clss="w-8 h-8 mr-2 shrink-0" icon={image} />
{message}
</button>
{feedback}
</div>
{:else}
<button class="no-image-background m-0 w-full p-4" on:click={() => apply()}>
<Icon clss="w-8 h-8 mr-2 shrink-0" icon={image} />
{message}
</button>
{/if}
{:else}
{message_closed}
{/if}
</LoginToggle>

View file

@ -20,7 +20,7 @@
tags,
keyToUse,
prefix,
postfix,
postfix
)
let currentState = oh.mapD((oh) => (typeof oh === "string" ? undefined : oh.getState()))
@ -29,12 +29,12 @@
let nextChange = oh
.mapD(
(oh) => (typeof oh === "string" ? undefined : oh.getNextChange(new Date(), tomorrow)),
[Stores.Chronic(5 * 60 * 1000)],
[Stores.Chronic(5 * 60 * 1000)]
)
.mapD((date) => Utils.TwoDigits(date.getHours()) + ":" + Utils.TwoDigits(date.getMinutes()))
let size = nextChange.map((change) =>
change === undefined ? "absolute h-7 w-7" : "absolute h-5 w-5 top-0 left-1/4",
change === undefined ? "absolute h-7 w-7" : "absolute h-5 w-5 top-0 left-1/4"
)
</script>

View file

@ -26,7 +26,7 @@
const dispatch = createEventDispatcher<{ selected: string }>()
let collapsedMode = true
let options: UIEventSource<PlantNetSpeciesMatch[]> = new UIEventSource<PlantNetSpeciesMatch[]>(
undefined,
undefined
)
let error: string = undefined

View file

@ -44,7 +44,7 @@
preset: PresetConfig
layer: LayerConfig
icon: BaseUIElement
tags: Record<string, string>,
tags: Record<string, string>
text: Translation
} = undefined
let checkedOfGlobalFilters: number = 0
@ -201,7 +201,7 @@
state.guistate.openFilterView(selectedPreset.layer)
}}
>
<Layers class="w-12"/>
<Layers class="w-12" />
<Tr t={Translations.t.general.add.openLayerControl} />
</button>
@ -242,7 +242,7 @@
state.guistate.openFilterView(selectedPreset.layer)
}}
>
<Layers class="w-12"/>
<Layers class="w-12" />
<Tr t={Translations.t.general.add.openLayerControl} />
</button>
</div>
@ -285,7 +285,7 @@
<NextButton on:click={() => (confirmedCategory = true)} clss="primary w-full">
<div slot="image" class="relative">
<ToSvelte construct={selectedPreset.icon}/>
<ToSvelte construct={selectedPreset.icon} />
<Confirm class="absolute bottom-0 right-0 h-4 w-4" />
</div>
<div class="w-full">
@ -304,7 +304,7 @@
<Tr
slot="message"
t={_globalFilter[checkedOfGlobalFilters].onNewPoint?.confirmAddNew.Subs({
preset: selectedPreset.text
preset: selectedPreset.text,
})}
/>
</SubtleButton>

View file

@ -26,7 +26,7 @@
/**
* Same as `this.preset.description.firstSentence()`
*/
description: Translation,
description: Translation
icon: BaseUIElement
tags: Record<string, string>
}[] = []
@ -40,7 +40,7 @@
"Not showing presets for layer",
flayer.layerDef.id,
"as not displayed and featureSwitchFilter.data is set",
state.featureSwitches.featureSwitchFilter.data,
state.featureSwitches.featureSwitchFilter.data
)
// ...and we cannot enable the layer control -> we skip, as these presets can never be shown anyway
continue
@ -55,9 +55,18 @@
for (const preset of layer.presets) {
const tags = TagUtils.KVtoProperties(preset.tags ?? [])
const markers = layer.mapRendering.map((mr, i) => mr.RenderIcon(new ImmutableStore<any>(tags), {noSize: i == 0})
.html.SetClass(i == 0 ? "w-full h-full" : ""))
const icon: BaseUIElement = new Combine(markers.map(m => new Combine([m]).SetClass("absolute top-0 left-0 w-full h-full flex justify-around items-center"))).SetClass("w-12 h-12 block relative mr-4")
const markers = layer.mapRendering.map((mr, i) =>
mr
.RenderIcon(new ImmutableStore<any>(tags), { noSize: i == 0 })
.html.SetClass(i == 0 ? "w-full h-full" : "")
)
const icon: BaseUIElement = new Combine(
markers.map((m) =>
new Combine([m]).SetClass(
"absolute top-0 left-0 w-full h-full flex justify-around items-center"
)
)
).SetClass("w-12 h-12 block relative mr-4")
const description = preset.description?.FirstSentence()
@ -69,7 +78,7 @@
tags,
text: Translations.t.general.add.addNew.Subs(
{ category: preset.title },
preset.title["context"],
preset.title["context"]
),
}
presets.push(simplified)
@ -78,10 +87,10 @@
const dispatch = createEventDispatcher<{
select: {
preset: PresetConfig;
layer: LayerConfig;
icon: BaseUIElement;
tags: Record<string, string>,
preset: PresetConfig
layer: LayerConfig
icon: BaseUIElement
tags: Record<string, string>
text: Translation
}
}>()

View file

@ -4,7 +4,7 @@
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
export let tags: UIEventSource<Record<string, any>>
export let tagKeys = tags.map(tgs => Object.keys(tgs))
export let tagKeys = tags.map(tgs => tgs === undefined ? [] : Object.keys(tgs))
export let layer: LayerConfig | undefined = undefined
@ -13,17 +13,16 @@
*/
let calculatedTags: string[] = []
for (const calculated of layer?.calculatedTags ?? []) {
if(calculated){
if (calculated) {
continue
}
const name = calculated[0]
calculatedTags.push(name)
}
let knownValues: Store<string[]> = tags.map(tags => Object.keys(tags))
let knownValues: Store<string[]> = tags.map((tags) => Object.keys(tags))
const metaKeys: string[] = [].concat(...SimpleMetaTaggers.metatags.map(k => k.keys))
const metaKeys: string[] = [].concat(...SimpleMetaTaggers.metatags.map((k) => k.keys))
let allCalculatedTags = new Set<string>([...calculatedTags, ...metaKeys])
</script>
<section>
@ -84,8 +83,15 @@
<tr>
<td>{key}</td>
<td>
{#if $knownValues.indexOf(key) < 0 }
<button class="small" on:click={_ => {console.log($tags[key])}}>Evaluate</button>
{#if $knownValues.indexOf(key) < 0}
<button
class="small"
on:click={(_) => {
console.log($tags[key])
}}
>
Evaluate
</button>
{:else if !$tags[key] === undefined}
<i>Undefined</i>
{:else if $tags[key] === ""}

View file

@ -10,7 +10,7 @@
import type { Feature } from "geojson"
import { UIEventSource } from "../../../Logic/UIEventSource"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import { TagUtils } from "../../../Logic/Tags/TagUtils"
import { TagUtils } from "../../../Logic/Tags/TagUtils"
import type { UploadableTag } from "../../../Logic/Tags/TagUtils"
import OsmChangeAction from "../../../Logic/Osm/Actions/OsmChangeAction"
import DeleteAction from "../../../Logic/Osm/Actions/DeleteAction"
@ -65,7 +65,7 @@
theme: state?.layout?.id ?? "unknown",
specialMotivation: deleteReason,
},
canBeDeleted.data,
canBeDeleted.data
)
} else {
// no _delete_reason is given, which implies that this is _not_ a deletion but merely a retagging via a nonDeleteMapping

View file

@ -53,7 +53,6 @@ export default class ConflateImportFlowState extends ImportFlow<ConflateFlowArgu
const action = this.action
await this.state.changes.applyAction(action)
const newId = action.newElementId ?? action.mainObjectId
this.state.selectedLayer.setData(this.targetLayer.layerDef)
this.state.selectedElement.setData(this.state.indexedFeatures.featuresById.data.get(newId))
}

View file

@ -1,58 +1,55 @@
<script lang="ts">
import type { ImportFlowArguments } from "./ImportFlow"
/**
* The 'importflow' does some basic setup, e.g. validate that imports are allowed, that the user is logged-in, ...
* They show some default components
*/
import ImportFlow from "./ImportFlow"
import LoginToggle from "../../Base/LoginToggle.svelte"
import BackButton from "../../Base/BackButton.svelte"
import Translations from "../../i18n/Translations"
import Tr from "../../Base/Tr.svelte"
import NextButton from "../../Base/NextButton.svelte"
import { createEventDispatcher, onDestroy } from "svelte"
import Loading from "../../Base/Loading.svelte"
import { And } from "../../../Logic/Tags/And"
import TagHint from "../TagHint.svelte"
import { TagsFilter } from "../../../Logic/Tags/TagsFilter"
import { Store } from "../../../Logic/UIEventSource"
import Svg from "../../../Svg"
import ToSvelte from "../../Base/ToSvelte.svelte"
import { EyeIcon, EyeOffIcon } from "@rgossiaux/svelte-heroicons/solid"
import FilteredLayer from "../../../Models/FilteredLayer"
import Confirm from "../../../assets/svg/Confirm.svelte"
import type { ImportFlowArguments } from "./ImportFlow"
/**
* The 'importflow' does some basic setup, e.g. validate that imports are allowed, that the user is logged-in, ...
* They show some default components
*/
import ImportFlow from "./ImportFlow"
import LoginToggle from "../../Base/LoginToggle.svelte"
import BackButton from "../../Base/BackButton.svelte"
import Translations from "../../i18n/Translations"
import Tr from "../../Base/Tr.svelte"
import NextButton from "../../Base/NextButton.svelte"
import { createEventDispatcher, onDestroy } from "svelte"
import Loading from "../../Base/Loading.svelte"
import { And } from "../../../Logic/Tags/And"
import TagHint from "../TagHint.svelte"
import { TagsFilter } from "../../../Logic/Tags/TagsFilter"
import { Store } from "../../../Logic/UIEventSource"
import Svg from "../../../Svg"
import ToSvelte from "../../Base/ToSvelte.svelte"
import { EyeIcon, EyeOffIcon } from "@rgossiaux/svelte-heroicons/solid"
import FilteredLayer from "../../../Models/FilteredLayer"
import Confirm from "../../../assets/svg/Confirm.svelte"
export let importFlow: ImportFlow<ImportFlowArguments>
let state = importFlow.state
export let importFlow: ImportFlow<ImportFlowArguments>
let state = importFlow.state
export let currentFlowStep: "start" | "confirm" | "importing" | "imported" = "start"
export let currentFlowStep: "start" | "confirm" | "importing" | "imported" = "start"
const isLoading = state.dataIsLoading
let dispatch = createEventDispatcher<{ confirm }>()
let canBeImported = importFlow.canBeImported()
let tags: Store<TagsFilter> = importFlow.tagsToApply.map((tags) => new And(tags))
const isLoading = state.dataIsLoading
let dispatch = createEventDispatcher<{ confirm }>()
let canBeImported = importFlow.canBeImported()
let tags: Store<TagsFilter> = importFlow.tagsToApply.map((tags) => new And(tags))
let targetLayers = importFlow.targetLayer
let filteredLayer: FilteredLayer
let undisplayedLayer: FilteredLayer
let targetLayers = importFlow.targetLayer
let filteredLayer: FilteredLayer
let undisplayedLayer: FilteredLayer
function updateIsDisplayed() {
filteredLayer = targetLayers.find((tl) => tl.hasFilter.data)
undisplayedLayer = targetLayers.find((tl) => !tl.isDisplayed.data)
}
function updateIsDisplayed() {
filteredLayer = targetLayers.find(tl => tl.hasFilter.data)
undisplayedLayer = targetLayers.find(tl => !tl.isDisplayed.data)
}
updateIsDisplayed()
updateIsDisplayed()
for (const tl of targetLayers) {
onDestroy(
tl.isDisplayed.addCallback(updateIsDisplayed),
)
}
for (const tl of targetLayers) {
onDestroy(tl.isDisplayed.addCallback(updateIsDisplayed))
}
function abort() {
state.selectedElement.setData(undefined)
}
function abort() {
state.selectedElement.setData(undefined)
}
</script>
<LoginToggle {state}>
@ -160,7 +157,7 @@
{#if importFlow.args.icon}
<img src={importFlow.args.icon} />
{:else}
<Confirm class="w-8 h-8 pr-4"/>
<Confirm class="h-8 w-8 pr-4" />
{/if}
</span>
<slot name="confirm-text">

View file

@ -24,7 +24,7 @@ export class ImportFlowUtils {
public static readonly conflationLayer = new LayerConfig(
<LayerConfigJson>conflation_json,
"all_known_layers",
true,
true
)
public static readonly documentationGeneral = `\n\n\nNote that the contributor must zoom to at least zoomlevel 18 to be able to use this functionality.
@ -66,7 +66,7 @@ ${Utils.special_visualizations_importRequirementDocs}
*/
public static getTagsToApply(
originalFeatureTags: UIEventSource<any>,
args: { tags: string },
args: { tags: string }
): Store<Tag[]> {
if (originalFeatureTags === undefined) {
return undefined
@ -82,9 +82,9 @@ ${Utils.special_visualizations_importRequirementDocs}
const items: string = originalFeatureTags.data[tags]
console.debug(
"The import button is using tags from properties[" +
tags +
"] of this object, namely ",
items,
tags +
"] of this object, namely ",
items
)
if (items.startsWith("{")) {
@ -120,7 +120,7 @@ ${Utils.special_visualizations_importRequirementDocs}
name: string
defaultValue?: string
}[],
argsRaw: string[],
argsRaw: string[]
): string[] {
const deps = ImportFlowUtils.getLayerDependencies(argsRaw, argSpec)
const argsParsed: PointImportFlowArguments = <any>Utils.ParseVisArgs(argSpec, argsRaw)
@ -146,13 +146,13 @@ export default abstract class ImportFlow<ArgT extends ImportFlowArguments> {
state: SpecialVisualizationState,
args: ArgT,
tagsToApply: Store<Tag[]>,
originalTags: UIEventSource<Record<string, string>>,
originalTags: UIEventSource<Record<string, string>>
) {
this.state = state
this.args = args
this.tagsToApply = tagsToApply
this._originalFeatureTags = originalTags
this.targetLayer = args.targetLayer.split(" ").map(tl => {
this.targetLayer = args.targetLayer.split(" ").map((tl) => {
let found = state.layerState.filteredLayers.get(tl)
if (!found) {
throw "Layer " + tl + " not found"
@ -198,7 +198,7 @@ export default abstract class ImportFlow<ArgT extends ImportFlowArguments> {
return undefined
},
[state.mapProperties.zoom, state.dataIsLoading, this._originalFeatureTags],
[state.mapProperties.zoom, state.dataIsLoading, this._originalFeatureTags]
)
}
}

View file

@ -17,7 +17,7 @@ export class PointImportButtonViz implements SpecialVisualization {
public readonly funcName: string
public readonly docs: string | BaseUIElement
public readonly example?: string
public readonly args: { name: string; defaultValue?: string; doc: string, split?: boolean }[]
public readonly args: { name: string; defaultValue?: string; doc: string; split?: boolean }[]
public needsUrls = []
constructor() {

View file

@ -13,7 +13,9 @@
const args = importFlow.args
// The following variables are used for the map
const targetLayers: LayerConfig[] = args.targetLayer.split(" ").map(tl => state.layout.layers.find((l) => l.id === tl))
const targetLayers: LayerConfig[] = args.targetLayer
.split(" ")
.map((tl) => state.layout.layers.find((l) => l.id === tl))
const snapToLayers: string[] | undefined =
args.snap_onto_layers?.split(",")?.map((l) => l.trim()) ?? []
const maxSnapDistance: number = Number(args.max_snap_distance ?? 25) ?? 25

View file

@ -126,7 +126,6 @@ export default class WayImportFlowState extends ImportFlow<WayImportFlowArgument
const action = this.action
await this.state.changes.applyAction(action)
const newId = action.newElementId ?? action.mainObjectId
this.state.selectedLayer.setData(this.targetLayer.layerDef)
this.state.selectedElement.setData(this.state.indexedFeatures.featuresById.data.get(newId))
}

View file

@ -25,19 +25,30 @@
</script>
{#if $languages.length === 1}
<SpecialTranslation {state} {tags} {feature} {layer}
t={new TypedTranslation({"*": single_render}).PartialSubsTr(
"language()",
new Translation(all_languages[$languages[0]], undefined)
)}/>
<SpecialTranslation
{state}
{tags}
{feature}
{layer}
t={new TypedTranslation({ "*": single_render }).PartialSubsTr(
"language()",
new Translation(all_languages[$languages[0]], undefined)
)}
/>
{:else}
{beforeListing}
<ul>
{#each $languages as language}
<li>
<SpecialTranslation {state} {tags} {feature} {layer} t={
new TypedTranslation({"*": item_render}).PartialSubsTr("language()",
new Translation(all_languages[language], undefined) )}
<SpecialTranslation
{state}
{tags}
{feature}
{layer}
t={new TypedTranslation({ "*": item_render }).PartialSubsTr(
"language()",
new Translation(all_languages[language], undefined)
)}
/>
</li>
{/each}

View file

@ -37,33 +37,48 @@
})
const forceInputMode = new UIEventSource(false)
</script>
{#if $foundLanguages.length === 0 && on_no_known_languages && !$forceInputMode}
<div class="p-1 flex items-center justify-between low-interaction rounded">
<div class="low-interaction flex items-center justify-between rounded p-1">
<div>
{on_no_known_languages}
</div>
<EditButton on:click={_ => forceInputMode.setData(true)} />
<EditButton on:click={(_) => forceInputMode.setData(true)} />
</div>
{:else if $forceInputMode || $foundLanguages.length === 0}
<LanguageQuestion {question} {foundLanguages} {prefix} {state} {tags} {feature} {layer}
on:save={_ => forceInputMode.setData(false)}>
<LanguageQuestion
{question}
{foundLanguages}
{prefix}
{state}
{tags}
{feature}
{layer}
on:save={(_) => forceInputMode.setData(false)}
>
<span slot="cancel-button">
{#if $forceInputMode}
<button on:click={_ => forceInputMode.setData(false)}>
<Tr t={Translations.t.general.cancel} />
</button>
{#if $forceInputMode}
<button on:click={(_) => forceInputMode.setData(false)}>
<Tr t={Translations.t.general.cancel} />
</button>
{/if}
</span>
</LanguageQuestion>
{:else}
<div class="p-2 flex items-center justify-between low-interaction rounded">
<div class="low-interaction flex items-center justify-between rounded p-2">
<div>
<LanguageAnswer {single_render} {item_render} {render_all} languages={foundLanguages} {state} {tags} { feature}
{layer} />
<LanguageAnswer
{single_render}
{item_render}
{render_all}
languages={foundLanguages}
{state}
{tags}
{feature}
{layer}
/>
</div>
<EditButton on:click={_ => forceInputMode.setData(true)} />
<EditButton on:click={(_) => forceInputMode.setData(true)} />
</div>
{/if}

View file

@ -1,121 +1,119 @@
<script lang="ts">/**
* An input element which allows to select one or more langauges
*/
import { UIEventSource } from "../../../Logic/UIEventSource"
import all_languages from "../../../assets/language_translations.json"
import { Translation } from "../../i18n/Translation"
import Tr from "../../Base/Tr.svelte"
import Translations from "../../i18n/Translations.js"
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid"
import Locale from "../../i18n/Locale"
<script lang="ts">
/**
* An input element which allows to select one or more langauges
*/
import { UIEventSource } from "../../../Logic/UIEventSource"
import all_languages from "../../../assets/language_translations.json"
import { Translation } from "../../i18n/Translation"
import Tr from "../../Base/Tr.svelte"
import Translations from "../../i18n/Translations.js"
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid"
import Locale from "../../i18n/Locale"
/**
* Will contain one or more ISO-language codes
*/
export let selectedLanguages: UIEventSource<string[]>
/**
* The country (countries) that the point lies in.
* Note that a single place might be claimed by multiple countries
*/
export let countries: Set<string>
let searchValue: UIEventSource<string> = new UIEventSource<string>("")
let searchLC = searchValue.mapD(search => search.toLowerCase())
const knownLanguagecodes = Object.keys(all_languages)
let probableLanguages = []
let isChecked = {}
for (const lng of knownLanguagecodes) {
const lngInfo = all_languages[lng]
if (lngInfo._meta?.countries?.some(l => countries.has(l))) {
probableLanguages.push(lng)
/**
* Will contain one or more ISO-language codes
*/
export let selectedLanguages: UIEventSource<string[]>
/**
* The country (countries) that the point lies in.
* Note that a single place might be claimed by multiple countries
*/
export let countries: Set<string>
let searchValue: UIEventSource<string> = new UIEventSource<string>("")
let searchLC = searchValue.mapD((search) => search.toLowerCase())
const knownLanguagecodes = Object.keys(all_languages)
let probableLanguages = []
let isChecked = {}
for (const lng of knownLanguagecodes) {
const lngInfo = all_languages[lng]
if (lngInfo._meta?.countries?.some((l) => countries.has(l))) {
probableLanguages.push(lng)
}
isChecked[lng] = false
}
isChecked[lng] = false
}
let newlyChecked: UIEventSource<string[]> = new UIEventSource<string[]>([])
let newlyChecked: UIEventSource<string[]> = new UIEventSource<string[]>([])
function update(isChecked: Record<string, boolean>) {
const currentlyChecked = new Set<string>(selectedLanguages.data)
const languages: string[] = []
for (const lng in isChecked) {
if (isChecked[lng]) {
languages.push(lng)
if (!currentlyChecked.has(lng)) {
newlyChecked.data.push(lng)
newlyChecked.ping()
function update(isChecked: Record<string, boolean>) {
const currentlyChecked = new Set<string>(selectedLanguages.data)
const languages: string[] = []
for (const lng in isChecked) {
if (isChecked[lng]) {
languages.push(lng)
if (!currentlyChecked.has(lng)) {
newlyChecked.data.push(lng)
newlyChecked.ping()
}
}
}
selectedLanguages.setData(languages)
}
function matchesSearch(lng: string, searchLc: string | undefined): boolean {
if (!searchLc) {
return
}
if (lng.indexOf(searchLc) >= 0) {
return true
}
const languageInfo = all_languages[lng]
const native: string = languageInfo[lng]?.toLowerCase()
if (native?.indexOf(searchLc) >= 0) {
return true
}
const current: string = languageInfo[Locale.language.data]?.toLowerCase()
if (current?.indexOf(searchLc) >= 0) {
return true
}
return false
}
function onEnter() {
// we select the first match which is not yet checked
for (const lng of knownLanguagecodes) {
if (lng === searchLC.data) {
isChecked[lng] = true
return
}
}
for (const lng of knownLanguagecodes) {
if (matchesSearch(lng, searchLC.data)) {
isChecked[lng] = true
return
}
}
}
selectedLanguages.setData(languages)
}
function matchesSearch(lng: string, searchLc: string | undefined): boolean {
if(!searchLc){
return
$: {
update(isChecked)
}
if(lng.indexOf(searchLc) >= 0){
return true
}
const languageInfo = all_languages[lng]
const native : string = languageInfo[lng]?.toLowerCase()
if(native?.indexOf(searchLc) >= 0){
return true
}
const current : string = languageInfo[Locale.language.data]?.toLowerCase()
if(current?.indexOf(searchLc) >= 0){
return true
}
return false
}
function onEnter(){
// we select the first match which is not yet checked
for (const lng of knownLanguagecodes) {
if(lng === searchLC.data){
isChecked[lng] = true
return
}
}
for (const lng of knownLanguagecodes) {
if(matchesSearch(lng, searchLC.data)){
isChecked[lng] = true
return
}
}
}
$: {
update(isChecked)
}
searchValue.addCallback(_ => {
newlyChecked.setData([])
})
searchValue.addCallback((_) => {
newlyChecked.setData([])
})
</script>
<form on:submit|preventDefault={() => onEnter()}>
{#each probableLanguages as lng}
<label class="no-image-background flex items-center gap-1">
<input bind:checked={isChecked[lng]} type="checkbox" />
<Tr t={new Translation(all_languages[lng])} />
<span class="subtle">({lng})</span>
</label>
{/each}
<label class="block relative neutral-label m-4 mx-16">
<SearchIcon class="w-6 h-6 absolute right-0" />
<label class="neutral-label relative m-4 mx-16 block">
<SearchIcon class="absolute right-0 h-6 w-6" />
<input bind:value={$searchValue} type="text" />
<Tr t={Translations.t.general.useSearch} />
</label>
<div class="overflow-auto" style="max-height: 25vh">
{#each knownLanguagecodes as lng}
{#if (isChecked[lng]) && $newlyChecked.indexOf(lng) < 0 && probableLanguages.indexOf(lng) < 0}
{#if isChecked[lng] && $newlyChecked.indexOf(lng) < 0 && probableLanguages.indexOf(lng) < 0}
<label class="no-image-background flex items-center gap-1">
<input bind:checked={isChecked[lng]} type="checkbox" />
<Tr t={new Translation(all_languages[lng])} />
<span class="subtle">({lng})</span>
</label>
{/if}
{/each}
@ -126,7 +124,6 @@ searchValue.addCallback(_ => {
<Tr t={new Translation(all_languages[lng])} />
<span class="subtle">({lng})</span>
</label>
{/if}
{/each}
</div>

View file

@ -1,86 +1,87 @@
<script lang="ts">/**
* The 'languageQuestion' is a special element which asks about the (possible) languages of a feature
* (e.g. which speech output an ATM has, in what language(s) the braille writing is or what languages are spoken at a school)
*
* This is written into a `key`.
*
*/
import { Translation } from "../../i18n/Translation"
import SpecialTranslation from "../TagRendering/SpecialTranslation.svelte"
import type { SpecialVisualizationState } from "../../SpecialVisualization"
import type { Store } from "../../../Logic/UIEventSource"
import { UIEventSource } from "../../../Logic/UIEventSource"
<script lang="ts">
/**
* The 'languageQuestion' is a special element which asks about the (possible) languages of a feature
* (e.g. which speech output an ATM has, in what language(s) the braille writing is or what languages are spoken at a school)
*
* This is written into a `key`.
*
*/
import { Translation } from "../../i18n/Translation"
import SpecialTranslation from "../TagRendering/SpecialTranslation.svelte"
import type { SpecialVisualizationState } from "../../SpecialVisualization"
import type { Store } from "../../../Logic/UIEventSource"
import { UIEventSource } from "../../../Logic/UIEventSource"
import type { Feature } from "geojson"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import LanguageOptions from "./LanguageOptions.svelte"
import Translations from "../../i18n/Translations"
import Tr from "../../Base/Tr.svelte"
import { createEventDispatcher } from "svelte"
import { Tag } from "../../../Logic/Tags/Tag"
import ChangeTagAction from "../../../Logic/Osm/Actions/ChangeTagAction"
import { And } from "../../../Logic/Tags/And"
import type { Feature } from "geojson"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import LanguageOptions from "./LanguageOptions.svelte"
import Translations from "../../i18n/Translations"
import Tr from "../../Base/Tr.svelte"
import { createEventDispatcher } from "svelte"
import { Tag } from "../../../Logic/Tags/Tag"
import ChangeTagAction from "../../../Logic/Osm/Actions/ChangeTagAction"
import { And } from "../../../Logic/Tags/And"
export let question: string
export let prefix: string
export let question: string
export let prefix: string
export let foundLanguages: Store<string[]>
export let state: SpecialVisualizationState
export let tags: UIEventSource<Record<string, string>>
export let feature: Feature
export let layer: LayerConfig | undefined
let dispatch = createEventDispatcher<{ save }>()
export let foundLanguages: Store<string[]>
export let state: SpecialVisualizationState
export let tags: UIEventSource<Record<string, string>>
export let feature: Feature
export let layer: LayerConfig | undefined
let dispatch = createEventDispatcher<{ save }>()
let selectedLanguages: UIEventSource<string[]> = new UIEventSource<string[]>([])
let countries: Store<Set<string>> = tags.mapD(tags => new Set<string>(tags["_country"]?.toUpperCase()?.split(";") ?? []))
async function applySelectedLanguages() {
const selectedLngs = selectedLanguages.data
const selection: Tag[] = selectedLanguages.data.map((ln) => new Tag(prefix + ln, "yes"))
if (selection.length === 0) {
return
}
const currentLanguages = foundLanguages.data
for (const currentLanguage of currentLanguages) {
if (selectedLngs.indexOf(currentLanguage) >= 0) {
continue
let selectedLanguages: UIEventSource<string[]> = new UIEventSource<string[]>([])
let countries: Store<Set<string>> = tags.mapD(
(tags) => new Set<string>(tags["_country"]?.toUpperCase()?.split(";") ?? [])
)
async function applySelectedLanguages() {
const selectedLngs = selectedLanguages.data
const selection: Tag[] = selectedLanguages.data.map((ln) => new Tag(prefix + ln, "yes"))
if (selection.length === 0) {
return
}
// Erase languages that are not spoken anymore
selection.push(new Tag(prefix + currentLanguage, ""))
}
const currentLanguages = foundLanguages.data
if (state === undefined || state?.featureSwitchIsTesting?.data) {
for (const tag of selection) {
tags.data[tag.key] = tag.value
for (const currentLanguage of currentLanguages) {
if (selectedLngs.indexOf(currentLanguage) >= 0) {
continue
}
// Erase languages that are not spoken anymore
selection.push(new Tag(prefix + currentLanguage, ""))
}
tags.ping()
} else if (state.changes) {
await state.changes
.applyAction(
new ChangeTagAction(
tags.data.id,
new And(selection),
tags.data,
{
theme: state?.layout?.id ?? "unkown",
changeType: "answer",
},
),
if (state === undefined || state?.featureSwitchIsTesting?.data) {
for (const tag of selection) {
tags.data[tag.key] = tag.value
}
tags.ping()
} else if (state.changes) {
await state.changes.applyAction(
new ChangeTagAction(tags.data.id, new And(selection), tags.data, {
theme: state?.layout?.id ?? "unkown",
changeType: "answer",
})
)
}
dispatch("save")
}
dispatch("save")
}
</script>
<div class="flex flex-col disable-links interactive border-interactive p-2">
<div class="disable-links interactive border-interactive flex flex-col p-2">
<div class="interactive justify-between pt-1 font-bold">
<SpecialTranslation {feature} {layer} {state} t={new Translation({"*":question})} {tags} />
<SpecialTranslation {feature} {layer} {state} t={new Translation({ "*": question })} {tags} />
</div>
<LanguageOptions {selectedLanguages} countries={$countries}/>
<LanguageOptions {selectedLanguages} countries={$countries} />
<div class="flex justify-end flex-wrap-reverse w-full">
<slot name="cancel-button"></slot>
<button class="primary" class:disabled={$selectedLanguages.length === 0} on:click={_ => applySelectedLanguages()}>
<div class="flex w-full flex-wrap-reverse justify-end">
<slot name="cancel-button" />
<button
class="primary"
class:disabled={$selectedLanguages.length === 0}
on:click={(_) => applySelectedLanguages()}
>
<Tr t={Translations.t.general.save} />
</button>
</div>

View file

@ -50,120 +50,121 @@
let notAllowed = moveWizardState.moveDisallowedReason
let currentMapProperties: MapProperties = undefined
</script>
<LoginToggle {state}>
{#if moveWizardState.reasons.length > 0}
{#if $notAllowed}
<div class="m-2 flex rounded-lg bg-gray-200 p-2">
<Move_not_allowed class="m-2 h-8 w-8" />
<div class="flex flex-col">
<Tr t={t.cannotBeMoved} />
<Tr t={$notAllowed} />
{#if moveWizardState.reasons.length > 0}
{#if $notAllowed}
<div class="m-2 flex rounded-lg bg-gray-200 p-2">
<Move_not_allowed class="m-2 h-8 w-8" />
<div class="flex flex-col">
<Tr t={t.cannotBeMoved} />
<Tr t={$notAllowed} />
</div>
</div>
</div>
{:else if currentStep === "start"}
{#if moveWizardState.reasons.length === 1}
<button
class="flex"
on:click={() => {
reason.setData(moveWizardState.reasons[0])
currentStep = "pick_location"
}}
>
<ToSvelte
construct={moveWizardState.reasons[0].icon.SetStyle("height: 1.5rem; width: 1.5rem;")}
/>
<Tr t={Translations.T(moveWizardState.reasons[0].invitingText)} />
</button>
{:else}
<button
class="flex"
on:click={() => {
currentStep = "reason"
}}
>
<Move class="h-6 w-6" />
<Tr t={t.inviteToMove.generic} />
</button>
{/if}
{:else if currentStep === "reason"}
<div class="interactive border-interactive flex flex-col p-2">
<Tr cls="text-lg font-bold" t={t.whyMove} />
{#each moveWizardState.reasons as reasonSpec}
{:else if currentStep === "start"}
{#if moveWizardState.reasons.length === 1}
<button
class="flex"
on:click={() => {
reason.setData(reasonSpec)
reason.setData(moveWizardState.reasons[0])
currentStep = "pick_location"
}}
>
<ToSvelte construct={reasonSpec.icon.SetClass("w-16 h-16 pr-2")} />
<Tr t={Translations.T(reasonSpec.text)} />
<ToSvelte
construct={moveWizardState.reasons[0].icon.SetStyle("height: 1.5rem; width: 1.5rem;")}
/>
<Tr t={Translations.T(moveWizardState.reasons[0].invitingText)} />
</button>
{/each}
</div>
{:else if currentStep === "pick_location"}
<div class="border-interactive interactive flex flex-col p-2">
<Tr cls="text-lg font-bold" t={t.moveTitle} />
<div class="relative h-64 w-full">
<LocationInput
mapProperties={(currentMapProperties = initMapProperties())}
value={newLocation}
initialCoordinate={{ lon, lat }}
/>
<div class="absolute bottom-0 left-0">
<OpenBackgroundSelectorButton {state} />
</div>
</div>
{#if $reason.includeSearch}
<Geosearch bounds={currentMapProperties.bounds} clearAfterView={false} />
{/if}
<div class="flex flex-wrap">
<If
condition={currentMapProperties.zoom.mapD(
(zoom) => zoom >= Constants.minZoomLevelToAddNewPoint
)}
>
<button
class="primary flex w-full"
on:click={() => {
moveWizardState.moveFeature(newLocation.data, reason.data, featureToMove)
currentStep = "moved"
}}
>
<Move class="mr-2 h-6 w-6" />
<Tr t={t.confirmMove} />
</button>
<div slot="else" class="alert">
<Tr t={t.zoomInFurther} />
</div>
</If>
{:else}
<button
class="w-full"
class="flex"
on:click={() => {
currentStep = "start"
currentStep = "reason"
}}
>
<XCircleIcon class="mr-2 h-6 w-6" />
<Tr t={t.cancel} />
<Move class="h-6 w-6" />
<Tr t={t.inviteToMove.generic} />
</button>
{/if}
{:else if currentStep === "reason"}
<div class="interactive border-interactive flex flex-col p-2">
<Tr cls="text-lg font-bold" t={t.whyMove} />
{#each moveWizardState.reasons as reasonSpec}
<button
on:click={() => {
reason.setData(reasonSpec)
currentStep = "pick_location"
}}
>
<ToSvelte construct={reasonSpec.icon.SetClass("w-16 h-16 pr-2")} />
<Tr t={Translations.T(reasonSpec.text)} />
</button>
{/each}
</div>
{:else if currentStep === "pick_location"}
<div class="border-interactive interactive flex flex-col p-2">
<Tr cls="text-lg font-bold" t={t.moveTitle} />
<div class="relative h-64 w-full">
<LocationInput
mapProperties={(currentMapProperties = initMapProperties())}
value={newLocation}
initialCoordinate={{ lon, lat }}
/>
<div class="absolute bottom-0 left-0">
<OpenBackgroundSelectorButton {state} />
</div>
</div>
{#if $reason.includeSearch}
<Geosearch bounds={currentMapProperties.bounds} clearAfterView={false} />
{/if}
<div class="flex flex-wrap">
<If
condition={currentMapProperties.zoom.mapD(
(zoom) => zoom >= Constants.minZoomLevelToAddNewPoint
)}
>
<button
class="primary flex w-full"
on:click={() => {
moveWizardState.moveFeature(newLocation.data, reason.data, featureToMove)
currentStep = "moved"
}}
>
<Move class="mr-2 h-6 w-6" />
<Tr t={t.confirmMove} />
</button>
<div slot="else" class="alert">
<Tr t={t.zoomInFurther} />
</div>
</If>
<button
class="w-full"
on:click={() => {
currentStep = "start"
}}
>
<XCircleIcon class="mr-2 h-6 w-6" />
<Tr t={t.cancel} />
</button>
</div>
</div>
{:else if currentStep === "moved"}
<div class="flex flex-col">
<Tr cls="thanks" t={t.pointIsMoved} />
<button
on:click={() => {
currentStep = "reason"
}}
>
<Move class="h-6 w-6 pr-2" />
<Tr t={t.inviteToMoveAgain} />
</button>
</div>
</div>
{:else if currentStep === "moved"}
<div class="flex flex-col">
<Tr cls="thanks" t={t.pointIsMoved} />
<button
on:click={() => {
currentStep = "reason"
}}
>
<Move class="h-6 w-6 pr-2" />
<Tr t={t.inviteToMoveAgain} />
</button>
</div>
{/if}
{/if}
{/if}
</LoginToggle>

View file

@ -16,10 +16,7 @@ export class AddNoteCommentViz implements SpecialVisualization {
},
]
public constr(
state: SpecialVisualizationState,
tags: UIEventSource<Record<string, string>>
) {
public constr(state: SpecialVisualizationState, tags: UIEventSource<Record<string, string>>) {
return new SvelteUIElement(AddNoteComment, { state, tags })
}
}

View file

@ -75,7 +75,7 @@ export default class NoteCommentElement extends Combine {
const extension = link.substring(lastDotIndex + 1, link.length)
return Utils.imageExtensions.has(extension)
})
.filter(link => !link.startsWith("https://wiki.openstreetmap.org/wiki/File:"))
.filter((link) => !link.startsWith("https://wiki.openstreetmap.org/wiki/File:"))
let imagesEl: BaseUIElement = undefined
if (images.length > 0) {
const imageEls = images.map((i) =>

View file

@ -15,6 +15,6 @@
</script>
<a class="button flex w-full items-center" href={url} style="margin-left: 0">
<Envelope class="w-8 h-8 mr-4 shrink-0"/>
<Envelope class="mr-4 h-8 w-8 shrink-0" />
{button_text}
</a>

View file

@ -156,14 +156,13 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
await state.changes.applyAction(changeAction)
try {
state.selectedElement.setData(state.indexedFeatures.featuresById.data.get(targetId))
}catch (e) {
} catch (e) {
console.error(e)
}
const maproulette_id_key = args[4]
if (maproulette_id_key) {
const maproulette_id = tags.data[ maproulette_id_key]
const maproulette_feature= state.indexedFeatures.featuresById.data.get(
maproulette_id)
const maproulette_id = tags.data[maproulette_id_key]
const maproulette_feature = state.indexedFeatures.featuresById.data.get(maproulette_id)
const maproulette_task_id = Number(maproulette_feature.properties.mr_taskId)
await Maproulette.singleton.closeTask(maproulette_task_id, Maproulette.STATUS_FIXED, {
comment: "Tags are copied onto " + targetId + " with MapComplete",
@ -202,9 +201,12 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
})
).SetClass("subtle")
const self = this
const applied = new UIEventSource(tags?.data?.["mr_taskStatus"] !== undefined && tags?.data?.["mr_taskStatus"] !== "Created") // This will default to 'false' for non-maproulette challenges
const applied = new UIEventSource(
tags?.data?.["mr_taskStatus"] !== undefined &&
tags?.data?.["mr_taskStatus"] !== "Created"
) // This will default to 'false' for non-maproulette challenges
const applyButton = new SubtleButton(
new SvelteUIElement(Icon, {icon: image}),
new SvelteUIElement(Icon, { icon: image }),
new Combine([msg, tagsExplanation]).SetClass("flex flex-col")
).onClick(async () => {
applied.setData(true)

View file

@ -1,16 +1,16 @@
<script lang="ts">
import { PencilAltIcon } from "@rgossiaux/svelte-heroicons/solid";
import { ariaLabel } from "../../../Utils/ariaLabel.js";
import { Translation } from "../../i18n/Translation"
import { PencilAltIcon } from "@rgossiaux/svelte-heroicons/solid"
import { ariaLabel } from "../../../Utils/ariaLabel.js"
import { Translation } from "../../i18n/Translation"
/**
* A small, round button with an edit-icon (and aria-labels etc)
*/
/**
* What arialabel to apply onto this button?
*/
export let arialabel : Translation = undefined;
export let ariaLabelledBy: string = undefined
/**
* A small, round button with an edit-icon (and aria-labels etc)
*/
/**
* What arialabel to apply onto this button?
*/
export let arialabel: Translation = undefined
export let ariaLabelledBy: string = undefined
</script>
<button

View file

@ -23,12 +23,12 @@
throw "Config is undefined in tagRenderingAnswer"
}
let trs: Store<{ then: Translation; icon?: string; iconClass?: string }[]> = tags.mapD((tags) =>
Utils.NoNull(config?.GetRenderValues(tags)),
Utils.NoNull(config?.GetRenderValues(tags))
)
</script>
{#if config !== undefined && (config?.condition === undefined || config.condition.matchesProperties($tags))}
<div {id} class={twMerge("link-underline inline-block w-full", config?.classes , extraClasses)}>
<div {id} class={twMerge("link-underline inline-block w-full", config?.classes, extraClasses)}>
{#if $trs.length === 1}
<TagRenderingMapping mapping={$trs[0]} {tags} {state} {selectedElement} {layer} />
{/if}

View file

@ -34,7 +34,7 @@
onDestroy(
tags.addCallbackD((tags) => {
editMode = !config.IsKnown(tags)
}),
})
)
}
@ -104,12 +104,13 @@
{:else}
<div class="low-interaction flex items-center justify-between overflow-hidden rounded px-2">
<TagRenderingAnswer id={answerId} {config} {tags} {selectedElement} {state} {layer} />
<EditButton
<EditButton
arialabel={config.editButtonAriaLabel}
ariaLabelledBy={answerId}
on:click={() => {
editMode = true
}}/>
editMode = true
}}
/>
</div>
{/if}
{:else}

View file

@ -29,7 +29,10 @@
{#if mapping.icon !== undefined}
<div class="inline-flex items-center">
<Icon icon={mapping.icon} clss={twJoin(`mapping-icon-${mapping.iconClass ?? "small"}`, "mr-2")} />
<Icon
icon={mapping.icon}
clss={twJoin(`mapping-icon-${mapping.iconClass ?? "small"}`, "mr-2")}
/>
<SpecialTranslation t={mapping.then} {tags} {state} {layer} feature={selectedElement} />
</div>
{:else if mapping.then !== undefined}

View file

@ -47,7 +47,7 @@
// Will be bound if a freeform is available
let freeformInput = new UIEventSource<string>(tags?.[config.freeform?.key])
let freeformInputUnvalidated = new UIEventSource<string>(freeformInput.data)
let selectedMapping: number = undefined
/**
* A list of booleans, used if multiAnswer is set
@ -153,16 +153,20 @@
}
})
$: {
if (allowDeleteOfFreeform && $freeformInput === undefined && $freeformInputUnvalidated === "" && (mappings?.length ?? 0) === 0) {
if (
allowDeleteOfFreeform &&
$freeformInput === undefined &&
$freeformInputUnvalidated === "" &&
(mappings?.length ?? 0) === 0
) {
selectedTags = new Tag(config.freeform.key, "")
} else {
try {
selectedTags = config?.constructChangeSpecification(
$freeformInput,
selectedMapping,
checkedMappings,
tags.data,
tags.data
)
} catch (e) {
console.error("Could not calculate changeSpecification:", e)
@ -227,21 +231,19 @@
onDestroy(
state.osmConnection?.userDetails?.addCallbackAndRun((ud) => {
numberOfCs = ud.csCount
}),
})
)
}
</script>
{#if question !== undefined}
<div class="relative">
<form
class="interactive border-interactive relative flex flex-col overflow-y-auto px-2"
style="max-height: 75vh"
on:submit|preventDefault={() => onSave()}
>
<fieldset>
<legend>
<div class="interactive sticky top-0 justify-between pt-1 font-bold" style="z-index: 11">
<SpecialTranslation t={question} {tags} {state} {layer} feature={selectedElement} />
@ -396,13 +398,16 @@
<slot name="save-button" {selectedTags}>
{#if allowDeleteOfFreeform && (mappings?.length ?? 0) === 0 && $freeformInput === undefined && $freeformInputUnvalidated === ""}
<button class="primary flex" on:click|stopPropagation|preventDefault={onSave}>
<TrashIcon class="w-6 h-6 text-red-500" />
<Tr t={Translations.t.general.eraseValue}/>
<TrashIcon class="h-6 w-6 text-red-500" />
<Tr t={Translations.t.general.eraseValue} />
</button>
{:else}
<button
on:click={onSave}
class={twJoin(selectedTags === undefined ? "disabled" : "button-shadow", "primary")}
class={twJoin(
selectedTags === undefined ? "disabled" : "button-shadow",
"primary"
)}
>
<Tr t={Translations.t.general.save} />
</button>
@ -410,23 +415,21 @@
</slot>
</div>
{#if UserRelatedState.SHOW_TAGS_VALUES.indexOf($showTags) >= 0 || ($showTags === "" && numberOfCs >= Constants.userJourney.tagsVisibleAt) || $featureSwitchIsTesting || $featureSwitchIsDebugging}
<span class="flex flex-wrap justify-between">
<TagHint {state} tags={selectedTags} currentProperties={$tags} />
<span class="flex flex-wrap">
{#if $featureSwitchIsTesting}
Testmode &nbsp;
{/if}
{#if $featureSwitchIsTesting || $featureSwitchIsDebugging}
<span class="subtle">{config.id}</span>
{/if}
</span>
</span>
<span class="flex flex-wrap justify-between">
<TagHint {state} tags={selectedTags} currentProperties={$tags} />
<span class="flex flex-wrap">
{#if $featureSwitchIsTesting}
Testmode &nbsp;
{/if}
{#if $featureSwitchIsTesting || $featureSwitchIsDebugging}
<span class="subtle">{config.id}</span>
{/if}
</span>
</span>
{/if}
<slot name="under-buttons" />
</LoginToggle>
</fieldset>
</form>
</div>
{/if}

View file

@ -23,7 +23,7 @@ export class UploadToOsmViz implements SpecialVisualization {
const locations = state.historicalUserLocations.features.data
return new SvelteUIElement(UploadTraceToOsmUI, {
state,
trace: (title: string) => GeoOperations.toGpx(locations, title)
trace: (title: string) => GeoOperations.toGpx(locations, title),
})
}
}

View file

@ -10,12 +10,11 @@
<div class="flex h-screen flex-col overflow-hidden px-4">
<div class="flex justify-between">
<h2 class="flex items-center">
<EyeIcon class="w-6 pr-2" />
<Tr t={Translations.t.privacy.title} />
</h2>
<LanguagePicker availableLanguages={Translations.t.privacy.intro.SupportedLanguages()}/>
<h2 class="flex items-center">
<EyeIcon class="w-6 pr-2" />
<Tr t={Translations.t.privacy.title} />
</h2>
<LanguagePicker availableLanguages={Translations.t.privacy.intro.SupportedLanguages()} />
</div>
<div class="h-full overflow-auto border border-gray-500 p-4">
<PrivacyPolicy />

View file

@ -0,0 +1,95 @@
<script lang="ts">
/**
* Allows to import a 'mangrove' private key from backup
*/
import LoginToggle from "../Base/LoginToggle.svelte"
import FileSelector from "../Base/FileSelector.svelte"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import Tr from "../Base/Tr.svelte"
import Translations from "../i18n/Translations"
import { UIEventSource } from "../../Logic/UIEventSource"
export let state: SpecialVisualizationState
export let text: string
let error: string = undefined
let success: string = undefined
function importContent(str: string) {
const parsed = JSON.parse(str)
const format = {
"crv": "P-256",
"d": undefined,
"ext": true,
"key_ops": ["sign"],
"kty": "EC",
"x": undefined,
"y": undefined,
"metadata": "Mangrove private key",
}
const neededKeys = Object.keys(format)
for (const neededKey of neededKeys) {
const expected = format[neededKey]
const actual = parsed[neededKey]
if (actual === undefined) {
error = "Not a valid key. The value for " + neededKey + " is missing"
return
}
if (typeof expected === "string" && expected !== actual) {
error = "Not a valid key. The value for " + neededKey + " should be " + expected + " but is " + actual
return
}
}
const current: UIEventSource<string> = state.userRelatedState.mangroveIdentity.mangroveIdentity
const flattened = JSON.stringify(parsed, null, "")
if (flattened === current.data) {
success = "The imported key is identical to the existing key"
return
}
console.log("Got", flattened, current)
current.setData(flattened)
success = "Applied private key"
}
async function onImport(files: FileList) {
error = undefined
success = undefined
try {
const reader = new FileReader()
reader.readAsText(files[0], "UTF-8")
// here we tell the reader what to do when it's done reading...
const content = await new Promise<string>((resolve, reject) => {
reader.onload = readerEvent => {
resolve(<string>readerEvent.target.result)
}
})
importContent(content)
} catch (e) {
error = e
}
}
</script>
<LoginToggle {state}>
<div class="flex flex-col m-1">
<FileSelector accept="application/json" multiple={false} on:submit={e => onImport(e.detail)}>
{text}
</FileSelector>
{#if error}
<div class="alert">
<Tr t={Translations.t.general.error} /> {error}</div>
{/if}
{#if success}
<div class="thanks">
{success}
</div>
{/if}
</div>
</LoginToggle>

View file

@ -1,75 +1,88 @@
<script lang="ts">
import FeatureReviews from "../../Logic/Web/MangroveReviews"
import StarsBar from "./StarsBar.svelte"
import SpecialTranslation from "../Popup/TagRendering/SpecialTranslation.svelte"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import type { Feature } from "geojson"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import Translations from "../i18n/Translations"
import Checkbox from "../Base/Checkbox.svelte"
import Tr from "../Base/Tr.svelte"
import If from "../Base/If.svelte"
import Loading from "../Base/Loading.svelte"
import { Review } from "mangrove-reviews-typescript"
import { Utils } from "../../Utils"
import { placeholder } from "../../Utils/placeholder"
import { ExclamationTriangle } from "@babeard/svelte-heroicons/solid/ExclamationTriangle"
import FeatureReviews from "../../Logic/Web/MangroveReviews"
import StarsBar from "./StarsBar.svelte"
import SpecialTranslation from "../Popup/TagRendering/SpecialTranslation.svelte"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import type { Feature } from "geojson"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import Translations from "../i18n/Translations"
import Checkbox from "../Base/Checkbox.svelte"
import Tr from "../Base/Tr.svelte"
import If from "../Base/If.svelte"
import Loading from "../Base/Loading.svelte"
import { Review } from "mangrove-reviews-typescript"
import { Utils } from "../../Utils"
import { placeholder } from "../../Utils/placeholder"
import { ExclamationTriangle } from "@babeard/svelte-heroicons/solid/ExclamationTriangle"
export let state: SpecialVisualizationState
export let tags: UIEventSource<Record<string, string>>
export let feature: Feature
export let layer: LayerConfig
/**
* The form to create a new review.
* This is multi-stepped.
*/
export let reviews: FeatureReviews
export let state: SpecialVisualizationState
export let tags: UIEventSource<Record<string, string>>
export let feature: Feature
export let layer: LayerConfig
/**
* The form to create a new review.
* This is multi-stepped.
*/
export let reviews: FeatureReviews
let score = 0
let confirmedScore = undefined
let isAffiliated = new UIEventSource(false)
let opinion = new UIEventSource<string>(undefined)
let score = 0
let confirmedScore = undefined
let isAffiliated = new UIEventSource(false)
let opinion = new UIEventSource<string>(undefined)
const t = Translations.t.reviews
const t = Translations.t.reviews
let _state: "ask" | "saving" | "done" = "ask"
let _state: "ask" | "saving" | "done" = "ask"
const connection = state.osmConnection
let connection = state.osmConnection
const hasError: Store<undefined | "too_long"> = opinion.mapD(op => {
const tooLong = op.length > FeatureReviews.REVIEW_OPINION_MAX_LENGTH
if (tooLong) {
return "too_long"
}
return undefined
})
async function save() {
if (hasError.data) {
return
}
_state = "saving"
let nickname = undefined
if (connection.isLoggedIn.data) {
nickname = connection.userDetails.data.name
}
const review: Omit<Review, "sub"> = {
rating: confirmedScore,
opinion: opinion.data,
metadata: { nickname, is_affiliated: isAffiliated.data },
}
if (state.featureSwitchIsTesting?.data ?? true) {
console.log("Testing - not actually saving review", review)
await Utils.waitFor(1000)
} else {
await reviews.createReview(review)
}
_state = "done"
let hasError: Store<undefined | "too_long"> = opinion.mapD((op) => {
const tooLong = op.length > FeatureReviews.REVIEW_OPINION_MAX_LENGTH
if (tooLong) {
return "too_long"
}
return undefined
})
let uploadFailed: string = undefined
async function save() {
if (hasError.data) {
return
}
_state = "saving"
let nickname = undefined
if (connection.isLoggedIn.data) {
nickname = connection.userDetails.data.name
}
const review: Omit<Review, "sub"> = {
rating: confirmedScore,
opinion: opinion.data,
metadata: { nickname, is_affiliated: isAffiliated.data },
}
if (state.featureSwitchIsTesting?.data ?? true) {
console.log("Testing - not actually saving review", review)
await Utils.waitFor(1000)
} else {
try {
await reviews.createReview(review)
} catch (e) {
console.error("Could not create review due to", e)
uploadFailed = "" + e
}
}
_state = "done"
}
</script>
{#if _state === "done"}
{#if uploadFailed}
<div class="alert flex">
<ExclamationTriangle class="h-6 w-6" />
<Tr t={Translations.t.general.error} />
{uploadFailed}
</div>
{:else if _state === "done"}
<Tr cls="thanks w-full" t={t.saved} />
{:else if _state === "saving"}
<Loading>
@ -109,8 +122,13 @@
/>
{#if $hasError === "too_long"}
<div class="alert flex items-center px-2">
<ExclamationTriangle class="w-12 h-12"/>
<Tr t={t.too_long.Subs({max: FeatureReviews.REVIEW_OPINION_MAX_LENGTH, amount: $opinion?.length ?? 0})}> </Tr>
<ExclamationTriangle class="h-12 w-12" />
<Tr
t={t.too_long.Subs({
max: FeatureReviews.REVIEW_OPINION_MAX_LENGTH,
amount: $opinion?.length ?? 0,
})}
/>
</div>
{/if}
</label>
@ -126,8 +144,7 @@
<Tr t={t.reviewing_as.Subs({ nickname: state.osmConnection.userDetails.data.name })} />
<Tr slot="else" t={t.reviewing_as_anonymous} />
</If>
<button class="primary" class:disabled={$hasError !== undefined}
on:click={save}>
<button class="primary" class:disabled={$hasError !== undefined} on:click={save}>
<Tr t={t.save} />
</button>
</div>

View file

@ -0,0 +1,46 @@
<script lang="ts">
import type { SpecialVisualizationState } from "../SpecialVisualization"
import Translations from "../i18n/Translations"
import Tr from "../Base/Tr.svelte"
import LoginToggle from "../Base/LoginToggle.svelte"
import LoginButton from "../Base/LoginButton.svelte"
import SingleReview from "./SingleReview.svelte"
import Mangrove_logo from "../../assets/svg/Mangrove_logo.svelte"
/**
* A panel showing all the reviews by the logged-in user
*/
export let state: SpecialVisualizationState
let reviews = state.userRelatedState.mangroveIdentity.getAllReviews()
const t = Translations.t.reviews
</script>
<LoginToggle {state}>
<div slot="not-logged-in">
<LoginButton osmConnection={state.osmConnection}>
<Tr t={Translations.t.favouritePoi.loginToSeeList} />
</LoginButton>
</div>
{#if $reviews?.length > 0}
<div class="flex flex-col" on:keypress={(e) => console.log("Got keypress", e)}>
{#each $reviews as review (review.sub)}
<SingleReview {review} showSub={true} {state} />
{/each}
</div>
{:else}
<Tr t={t.your_reviews_empty} />
{/if}
<a
class="link-underline"
href="https://github.com/pietervdvn/MapComplete/issues/1782"
target="_blank"
rel="noopener noreferrer"
>
<Tr t={t.reviews_bug} />
</a>
<div class="flex justify-end">
<Mangrove_logo class="h-12 w-12 shrink-0 p-1" />
<Tr cls="text-sm subtle" t={t.attribution} />
</div>
</LoginToggle>

View file

@ -1,22 +1,49 @@
<script lang="ts">
import { Review } from "mangrove-reviews-typescript"
import { Store } from "../../Logic/UIEventSource"
import { ImmutableStore, Store } from "../../Logic/UIEventSource"
import StarsBar from "./StarsBar.svelte"
import Translations from "../i18n/Translations"
import Tr from "../Base/Tr.svelte"
import { ariaLabel } from "../../Utils/ariaLabel"
import type { SpecialVisualizationState } from "../SpecialVisualization"
export let review: Review & { kid: string; signature: string; madeByLoggedInUser: Store<boolean> }
export let state: SpecialVisualizationState = undefined
export let review: Review & {
kid: string
signature: string
madeByLoggedInUser?: Store<boolean>
}
let name = review.metadata.nickname
name ??= ((review.metadata.given_name ?? "") + " " + (review.metadata.family_name ?? "")).trim()
let d = new Date()
d.setTime(review.iat * 1000)
let date = d.toDateString()
let byLoggedInUser = review.madeByLoggedInUser
let byLoggedInUser = review.madeByLoggedInUser ?? ImmutableStore.FALSE
export let showSub = false
let subUrl = new URL(review.sub)
let [lat, lon] = subUrl.pathname.split(",").map((l) => Number(l))
let sub = subUrl.searchParams.get("q")
function selectFeature() {
console.log("Selecting and zooming to", { lon, lat })
state?.mapProperties?.location?.setData({ lon, lat })
state?.mapProperties?.zoom?.setData(Math.max(16, state?.mapProperties?.zoom?.data))
state?.guistate?.closeAll()
}
</script>
<div class={"low-interaction rounded-lg p-1 px-2 " + ($byLoggedInUser ? "border-interactive" : "")}>
<div class="flex items-center justify-between">
<div
class={"low-interaction flex flex-col rounded-lg p-1 px-2" +
($byLoggedInUser ? "border-interactive" : "")}
>
{#if showSub}
<button class="link" on:click={() => selectFeature()}>
<h3>{sub}</h3>
</button>
{/if}
<div class="flex w-full items-center justify-between">
<div
tabindex="0"
use:ariaLabel={Translations.t.reviews.rated.Subs({

View file

@ -55,14 +55,6 @@ export interface SpecialVisualizationState {
readonly mapProperties: MapProperties & ExportableMap
readonly selectedElement: UIEventSource<Feature>
/**
* Works together with 'selectedElement' to indicate what properties should be displayed
* @deprecated
*
* No need to set this anymore
*/
readonly selectedLayer: UIEventSource<LayerConfig>
readonly selectedElementAndLayer: Store<{ feature: Feature; layer: LayerConfig }>
readonly favourites: FavouritesFeatureSource

View file

@ -90,6 +90,9 @@ import Qr from "../Utils/Qr"
import ComparisonTool from "./Comparison/ComparisonTool.svelte"
import SpecialTranslation from "./Popup/TagRendering/SpecialTranslation.svelte"
import SpecialVisualisationUtils from "./SpecialVisualisationUtils"
import LoginButton from "./Base/LoginButton.svelte"
import Toggle from "./Input/Toggle"
import ImportReviewIdentity from "./Reviews/ImportReviewIdentity.svelte"
class NearbyImageVis implements SpecialVisualization {
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
@ -295,6 +298,7 @@ export default class SpecialVisualizations {
): RenderingSpecification[] {
return SpecialVisualisationUtils.constructSpecification(template, extraMappings)
}
public static HelpMessage() {
const helpTexts = SpecialVisualizations.specialVisualizations.map((viz) =>
SpecialVisualizations.DocumentationFor(viz)
@ -734,6 +738,19 @@ export default class SpecialVisualizations {
return new SvelteUIElement(AllReviews, { reviews, state, tags, feature, layer })
},
},
{
funcName: "import_mangrove_key",
docs: "Only makes sense in the usersettings. Allows to import a mangrove public key and to use this to make reviews",
args: [{
name: "text",
doc: "The text that is shown on the button",
}],
needsUrls: [],
constr(state: SpecialVisualizationState, tagSource: UIEventSource<Record<string, string>>, argument: string[], feature: Feature, layer: LayerConfig): BaseUIElement {
const [text] = argument
return new SvelteUIElement(ImportReviewIdentity, { state, text })
},
},
{
funcName: "opening_hours_table",
docs: "Creates an opening-hours table. Usage: {opening_hours_table(opening_hours)} to create a table of the tag 'opening_hours'.",
@ -754,7 +771,7 @@ export default class SpecialVisualizations {
doc: "Remove this string from the end of the value before parsing. __Note: use `&RPARENs` to indicate `)` if needed__",
},
],
needsUrls: [Constants.countryCoderEndpoint],
example:
"A normal opening hours table can be invoked with `{opening_hours_table()}`. A table for e.g. conditional access with opening hours can be `{opening_hours_table(access:conditional, no @ &LPARENS, &RPARENS)}`",
constr: (state, tagSource: UIEventSource<any>, args) => {
@ -1086,10 +1103,22 @@ export default class SpecialVisualizations {
doc: "The property name containing the maproulette id",
defaultValue: "mr_taskId",
},
{
name: "ask_feedback",
doc: "If not an empty string, this will be used as question to ask some additional feedback. A text field will be added",
defaultValue: "",
},
],
constr: (state, tagsSource, args) => {
let [message, image, message_closed, statusToSet, maproulette_id_key] = args
let [
message,
image,
message_closed,
statusToSet,
maproulette_id_key,
askFeedback,
] = args
if (image === "") {
image = "confirm"
}
@ -1105,6 +1134,7 @@ export default class SpecialVisualizations {
message_closed,
statusToSet,
maproulette_id_key,
askFeedback,
})
},
},
@ -1203,7 +1233,10 @@ export default class SpecialVisualizations {
(tags) =>
new SvelteUIElement(Link, {
text: Utils.SubstituteKeys(text, tags),
href: Utils.SubstituteKeys(href, tags).replaceAll(/ /g, '%20') /* Chromium based browsers eat the spaces */,
href: Utils.SubstituteKeys(href, tags).replaceAll(
/ /g,
"%20"
) /* Chromium based browsers eat the spaces */,
classnames,
download: Utils.SubstituteKeys(download, tags),
ariaLabel: Utils.SubstituteKeys(ariaLabel, tags),
@ -1666,6 +1699,25 @@ export default class SpecialVisualizations {
})
},
},
{
funcName: "login_button",
args: [],
docs: "Show a login button",
needsUrls: [],
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
args: string[],
feature: Feature,
layer: LayerConfig
): BaseUIElement {
return new Toggle(
undefined,
new SvelteUIElement(LoginButton),
state.osmConnection.isLoggedIn
)
},
},
]
specialVisualizations.push(new AutoApplyButton(specialVisualizations))

View file

@ -190,16 +190,17 @@ class StatsticsForOverviewFile extends Combine {
}
}
elements.push(new SubtleButton(
undefined, "Download as csv"
).onClick(() => {
const data = GeoOperations.toCSV(overview._meta,
{
ignoreTags: /^((deletion:node)|(import:node)|(move:node)|(soft-delete:))/
elements.push(
new SubtleButton(undefined, "Download as csv").onClick(() => {
const data = GeoOperations.toCSV(overview._meta, {
ignoreTags:
/^((deletion:node)|(import:node)|(move:node)|(soft-delete:))/,
})
Utils.offerContentsAsDownloadableFile(data , "statistics.csv", {mimetype: "text/csv"})
}))
Utils.offerContentsAsDownloadableFile(data, "statistics.csv", {
mimetype: "text/csv",
})
})
)
return new Combine(elements)
},

View file

@ -38,10 +38,5 @@
</script>
<div>
<TagRenderingEditable
{config}
selectedElement={undefined}
{state}
{tags}
/>
<TagRenderingEditable {config} selectedElement={undefined} {state} {tags} />
</div>

View file

@ -30,7 +30,7 @@
let thenText: UIEventSource<Record<string, string>> = state.getStoreFor([...path, "then"])
let thenTextEn = thenText.mapD((translation) =>
typeof translation === "string" ? translation : translation["en"],
typeof translation === "string" ? translation : translation["en"]
)
let editMode = Object.keys($thenText ?? {})?.length === 0
@ -72,7 +72,7 @@
<FromHtml src={$parsedTag?.asHumanString(false, false, $exampleTags)} />
{#if $messages.length > 0}
<div class="alert m-2 flex">
<ExclamationTriangle class="w-6 h-6" />
<ExclamationTriangle class="h-6 w-6" />
{$messages.length} errors
</div>
{/if}

View file

@ -83,7 +83,6 @@
helperArgs,
},
}
if (schema.hints.default) {
configJson.mappings = [
@ -95,7 +94,7 @@
schema.hints.default +
"</b> will be used. " +
(schema.hints.ifunset ?? ""),
hideInAnswer: mightBeBoolean(schema.type)
hideInAnswer: mightBeBoolean(schema.type),
},
]
} else if (!schema.required) {
@ -107,7 +106,6 @@
]
}
if (mightBeBoolean(schema.type)) {
configJson.mappings = configJson.mappings ?? []
configJson.mappings.push(
@ -147,19 +145,19 @@
tags.map((tgs) => {
const v = tgs["value"]
if (typeof v === "object") {
return { ...<object>v }
return { ...(<object>v) }
}
if (schema.type === "boolean") {
if(v === null || v === undefined){
return v
}
if (v === null || v === undefined) {
return v
}
return v === "true" || v === "yes" || v === "1"
}
if (mightBeBoolean(schema.type)) {
if (v === "true" || v === "yes" || v === "1") {
return true
}
if (v === "false" || v === "no" || v === "0" || (<any> v) === false) {
if (v === "false" || v === "no" || v === "0" || <any>v === false) {
return false
}
}

View file

@ -212,12 +212,7 @@
with MapComplete Studio
{:else}
<div>
<TagRenderingEditable
{config}
selectedElement={undefined}
{state}
{tags}
/>
<TagRenderingEditable {config} selectedElement={undefined} {state} {tags} />
</div>
{#if chosenOption !== undefined}

View file

@ -6,7 +6,7 @@
export let state: EditLayerState
export let path: (string | number)[] = []
let value = new UIEventSource<Record<string,string>>({})
let value = new UIEventSource<Record<string, string>>({})
console.log("Registering translation to path", path)
state.register(
path,

Some files were not shown because too many files have changed in this diff Show more