forked from MapComplete/MapComplete
Merge branch 'develop' into feature/json-editor
This commit is contained in:
commit
e932bfd9cd
201 changed files with 4529 additions and 4456 deletions
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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())]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}),
|
||||
})
|
||||
|
|
|
@ -115,7 +115,6 @@ export default class ThemeViewStateHashActor {
|
|||
""
|
||||
)
|
||||
selectedElement.setData(found)
|
||||
state.selectedLayer.setData(layer)
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
),
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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[]
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
onMount(() => {
|
||||
const uiElem = typeof construct === "function" ? construct() : construct
|
||||
html = uiElem?.ConstructElement()
|
||||
|
||||
|
||||
if (html !== undefined) {
|
||||
elem?.replaceWith(html)
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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() {}
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
19
src/UI/Map/StyleLoadingIndicator.svelte
Normal file
19
src/UI/Map/StyleLoadingIndicator.svelte
Normal 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}
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}>()
|
||||
|
|
|
@ -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] === ""}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) =>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
{/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
|
||||
{/if}
|
||||
{#if $featureSwitchIsTesting || $featureSwitchIsDebugging}
|
||||
<span class="subtle">{config.id}</span>
|
||||
{/if}
|
||||
</span>
|
||||
</span>
|
||||
{/if}
|
||||
<slot name="under-buttons" />
|
||||
</LoginToggle>
|
||||
</fieldset>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/if}
|
||||
|
|
|
@ -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),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 />
|
||||
|
|
95
src/UI/Reviews/ImportReviewIdentity.svelte
Normal file
95
src/UI/Reviews/ImportReviewIdentity.svelte
Normal 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>
|
|
@ -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>
|
||||
|
|
46
src/UI/Reviews/ReviewsOverview.svelte
Normal file
46
src/UI/Reviews/ReviewsOverview.svelte
Normal 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>
|
|
@ -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({
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
|
|
|
@ -38,10 +38,5 @@
|
|||
</script>
|
||||
|
||||
<div>
|
||||
<TagRenderingEditable
|
||||
{config}
|
||||
selectedElement={undefined}
|
||||
{state}
|
||||
{tags}
|
||||
/>
|
||||
<TagRenderingEditable {config} selectedElement={undefined} {state} {tags} />
|
||||
</div>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue