Stabilize personal theme, textfield now correctly appears if it is an option in the freeform too

This commit is contained in:
pietervdvn 2021-02-20 01:45:51 +01:00
parent 79fc3f54e5
commit 416a76ae4f
22 changed files with 278 additions and 149 deletions

View file

@ -74,15 +74,8 @@ export class InitUiElements {
}
InitUiElements.InitBaseMap();
InitUiElements.setupAllLayerElements();
if (layoutToUse.customCss !== undefined) {
Utils.LoadCustomCss(layoutToUse.customCss);
}
function updateFavs() {
// This is purely for the personal theme to load the layers there
const favs = State.state.favouriteLayers.data ?? [];
layoutToUse.layers.splice(0, layoutToUse.layers.length);
@ -103,19 +96,16 @@ export class InitUiElements {
}
}
}
InitUiElements.setupAllLayerElements();
State.state.layerUpdater.ForceRefresh();
State.state.layoutToUse.ping();
State.state.layerUpdater?.ForceRefresh();
}
if (layoutToUse.id === personal.id) {
State.state.favouriteLayers.addCallback(updateFavs);
State.state.installedThemes.addCallback(updateFavs);
if (layoutToUse.customCss !== undefined) {
Utils.LoadCustomCss(layoutToUse.customCss);
}
InitUiElements.InitBaseMap();
InitUiElements.OnlyIf(State.state.featureSwitchUserbadge, () => {
new UserBadge().AttachTo('userbadge');
@ -162,7 +152,17 @@ export class InitUiElements {
, State.state.featureSwitchGeolocation)
.AttachTo("geolocate-button");
updateFavs();
InitUiElements.setupAllLayerElements();
if (layoutToUse.id === personal.id) {
State.state.favouriteLayers.addCallback(updateFavs);
State.state.installedThemes.addCallback(updateFavs);
}else{
State.state.locationControl.ping();
}
// Reset the loading message once things are loaded
new CenterMessageBox().AttachTo("centermessage");
@ -209,7 +209,6 @@ export class InitUiElements {
const isOpened = new UIEventSource<boolean>(true);
const fullOptions = new FullWelcomePaneWithTabs(() => {
console.log("Closing the welcome message...")
isOpened.setData(false);
});
@ -325,7 +324,7 @@ export class InitUiElements {
const updater = new LoadFromOverpass(state.locationControl, state.layoutToUse, state.leafletMap);
State.state.layerUpdater = updater;
const source = new FeaturePipeline(state.filteredLayers.data, updater, state.layoutToUse, state.changes, state.locationControl);
const source = new FeaturePipeline(state.filteredLayers, updater, state.layoutToUse, state.changes, state.locationControl);
source.features.addCallbackAndRun((featuresFreshness: { feature: any, freshness: Date }[]) => {

View file

@ -13,7 +13,7 @@ export default class InstalledThemes {
return installedThemes;
}
const invalidThemes = []
for (var allPreferencesKey in allPreferences) {
for (const allPreferencesKey in allPreferences) {
const themename = allPreferencesKey.match(/^mapcomplete-installed-theme-(.*)-combined-length$/);
if (themename && themename[1] !== "") {
const customLayout = osmConnection.GetLongPreference("installed-theme-" + themename[1]);

View file

@ -44,10 +44,15 @@ export default class SelectedFeatureHandler {
// Feature already selected
return;
}
const hash = this._hash.data;
if(hash === undefined || hash === "" || hash === "#"){
return;
}
console.log("Selecting a feature from the hash...")
for (const feature of features) {
const id = feature.feature?.properties?.id;
if(id === this._hash.data){
if(id === hash){
this._selectedFeature.setData(feature.feature);
break;
}

View file

@ -17,8 +17,8 @@ export default class UpdateFromOverpass implements FeatureSource{
public readonly sufficientlyZoomed: UIEventSource<boolean>;
public readonly runningQuery: UIEventSource<boolean> = new UIEventSource<boolean>(false);
public readonly retries: UIEventSource<number> = new UIEventSource<number>(0);
public readonly timeout: UIEventSource<number> = new UIEventSource<number>(0);
private readonly retries: UIEventSource<number> = new UIEventSource<number>(0);
/**
* The previous bounds for which the query has been run at the given zoom level
*
@ -59,7 +59,7 @@ export default class UpdateFromOverpass implements FeatureSource{
layoutToUse.addCallback(() => {
self.update()
});
location.addCallbackAndRun(() => {
location.addCallback(() => {
self.update()
});
}
@ -109,6 +109,7 @@ export default class UpdateFromOverpass implements FeatureSource{
}
return new Or(filters);
}
private update(): void {
const filter = this.GetFilter();
if (filter === undefined) {
@ -145,21 +146,36 @@ export default class UpdateFromOverpass implements FeatureSource{
function (reason) {
self.retries.data++;
self.ForceRefresh();
console.log(`QUERY FAILED (retrying in ${5 * self.retries.data} sec)`, undefined);
self.timeout.setData(self.retries.data * 5);
console.log(`QUERY FAILED (retrying in ${5 * self.retries.data} sec)`, reason);
self.retries.ping();
self.runningQuery.setData(false)
self.runningQuery.setData(false);
function countDown() {
window?.setTimeout(
function () {
self.update()
}, self.retries.data * 5000
console.log("Countdown: ", self.timeout.data)
if (self.timeout.data > 1) {
self.timeout.setData(self.timeout.data - 1);
window.setTimeout(
countDown,
1000
)
} else {
self.timeout.setData(0);
self.update()
}
}, 1000
)
}
countDown();
}
);
}
private IsInBounds(bounds: Bounds): boolean {
if (this._previousBounds === undefined) {
return false;
@ -173,6 +189,4 @@ export default class UpdateFromOverpass implements FeatureSource{
}
}

View file

@ -12,7 +12,7 @@ export default class FeatureDuplicatorPerLayer implements FeatureSource {
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
constructor(layers: { layerDef: LayerConfig }[], upstream: FeatureSource) {
constructor(layers: UIEventSource<{ layerDef: LayerConfig }[]>, upstream: FeatureSource) {
this.features = upstream.features.map(features => {
const newFeatures: { feature: any, freshness: Date }[] = [];
if(features === undefined){
@ -29,7 +29,7 @@ export default class FeatureDuplicatorPerLayer implements FeatureSource {
let foundALayer = false;
for (const layer of layers) {
for (const layer of layers.data) {
if (layer.layerDef.overpassTags.matchesProperties(f.feature.properties)) {
foundALayer = true;
if (layer.layerDef.passAllFeatures) {

View file

@ -16,7 +16,7 @@ export default class FeaturePipeline implements FeatureSource {
public features: UIEventSource<{ feature: any; freshness: Date }[]>;
constructor(flayers: { isDisplayed: UIEventSource<boolean>, layerDef: LayerConfig }[],
constructor(flayers: UIEventSource<{ isDisplayed: UIEventSource<boolean>, layerDef: LayerConfig }[]>,
updater: FeatureSource,
layout: UIEventSource<LayoutConfig>,
newPoints: FeatureSource,

View file

@ -6,23 +6,24 @@ import Loc from "../../Models/Loc";
export default class FilteringFeatureSource implements FeatureSource {
public features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]);
constructor(layers: {
constructor(layers: UIEventSource<{
isDisplayed: UIEventSource<boolean>,
layerDef: LayerConfig
}[],
}[]>,
location: UIEventSource<Loc>,
upstream: FeatureSource) {
const self = this;
function update() {
const layerDict = {};
for (const layer of layers) {
for (const layer of layers.data) {
layerDict[layer.layerDef.id] = layer;
}
function update() {
console.log("Updating the filtering layer")
console.log("Updating the filtering layer, input ", upstream.features.data.length, "features")
const features: { feature: any, freshness: Date }[] = upstream.features.data;
@ -42,7 +43,7 @@ export default class FilteringFeatureSource implements FeatureSource {
}
}
// Does it match any other layer - e.g. because of a switch?
for (const toCheck of layers) {
for (const toCheck of layers.data) {
if (!FilteringFeatureSource.showLayer(toCheck, location)) {
continue;
}
@ -53,6 +54,8 @@ export default class FilteringFeatureSource implements FeatureSource {
return false;
});
console.log("Updating the filtering layer, output ", newFeatures.length, "features")
self.features.setData(newFeatures);
}
@ -63,21 +66,34 @@ export default class FilteringFeatureSource implements FeatureSource {
location.map(l => {
// We want something that is stable for the shown layers
const displayedLayerIndexes = [];
for (let i = 0; i < layers.length; i++) {
if (l.zoom < layers[i].layerDef.minzoom) {
for (let i = 0; i < layers.data.length; i++) {
const layer = layers.data[i];
if (l.zoom < layer.layerDef.minzoom) {
continue;
}
if (!layers[i].isDisplayed.data) {
if (!layer.isDisplayed.data) {
continue;
}
displayedLayerIndexes.push(i);
}
return displayedLayerIndexes.join(",")
}, layers.map(l => l.isDisplayed))
.addCallback(() => {
}).addCallback(() => {
update();
});
layers.addCallback(update);
const registered = new Set<UIEventSource<boolean>>();
layers.addCallback(layers => {
for (const layer of layers) {
if(registered.has(layer.isDisplayed)){
continue;
}
registered.add(layer.isDisplayed);
layer.isDisplayed.addCallback(update);
}
})
update();
}

View file

@ -9,6 +9,9 @@ export default class LocalStorageSource implements FeatureSource {
constructor(layout: UIEventSource<LayoutConfig>) {
this.features = new UIEventSource<{ feature: any; freshness: Date }[]>([])
const key = LocalStorageSaver.storageKey + layout.data.id
layout.addCallbackAndRun(_ => {
try {
const fromStorage = localStorage.getItem(key);
if (fromStorage == null) {
@ -21,6 +24,6 @@ export default class LocalStorageSource implements FeatureSource {
console.log("Could not load features from localStorage:", e)
localStorage.removeItem(key)
}
})
}
}

View file

@ -12,16 +12,12 @@ export default class NoOverlapSource {
features: UIEventSource<{ feature: any, freshness: Date }[]> = new UIEventSource<{ feature: any, freshness: Date }[]>([]);
constructor(layers: {
constructor(layers: UIEventSource<{
layerDef: LayerConfig
}[],
}[]>,
upstream: FeatureSource) {
const layerDict = {};
let noOverlapRemoval = true;
const layerIds = []
for (const layer of layers) {
layerDict[layer.layerDef.id] = layer;
layerIds.push(layer.layerDef.id);
for (const layer of layers.data) {
if ((layer.layerDef.hideUnderlayingFeaturesMinPercentage ?? 0) !== 0) {
noOverlapRemoval = false;
}
@ -31,13 +27,22 @@ export default class NoOverlapSource {
return;
}
this.features = upstream.features.map(
features => {
if (features === undefined) {
return;
}
const layerIds = []
const layerDict = {};
for (const layer of layers.data) {
layerDict[layer.layerDef.id] = layer;
layerIds.push(layer.layerDef.id);
if ((layer.layerDef.hideUnderlayingFeaturesMinPercentage ?? 0) !== 0) {
noOverlapRemoval = false;
}
}
// There is overlap removal active
// We partition all the features with their respective layerIDs
const partitions = {};

View file

@ -6,29 +6,26 @@ import {GeoOperations} from "../GeoOperations";
export default class WayHandlingApplyingFeatureSource implements FeatureSource {
features: UIEventSource<{ feature: any; freshness: Date }[]>;
constructor(layers: {
constructor(layers: UIEventSource<{
layerDef: LayerConfig
}[],
}[]>,
upstream: FeatureSource) {
const layerDict = {};
let allDefaultWayHandling = true;
for (const layer of layers) {
layerDict[layer.layerDef.id] = layer;
if (layer.layerDef.wayHandling !== LayerConfig.WAYHANDLING_DEFAULT) {
allDefaultWayHandling = false;
}
}
if (allDefaultWayHandling) {
this.features = upstream.features;
return;
}
this.features = upstream.features.map(
features => {
if(features === undefined){
return;
}
const layerDict = {};
let allDefaultWayHandling = true;
for (const layer of layers.data) {
layerDict[layer.layerDef.id] = layer;
if (layer.layerDef.wayHandling !== LayerConfig.WAYHANDLING_DEFAULT) {
allDefaultWayHandling = false;
}
}
const newFeatures: { feature: any, freshness: Date }[] = [];
for (const f of features) {
const feat = f.feature;

View file

@ -123,7 +123,8 @@ export default class State {
this.layoutToUse.setData(layoutToUse);
// -- Location control initialization
{ const zoom = State.asFloat(
{
const zoom = State.asFloat(
QueryParameters.GetQueryParameter("z", "" + (layoutToUse?.startZoom ?? 1), "The initial/current zoom level")
.syncWith(LocalStorageSource.Get("zoom")));
const lat = State.asFloat(QueryParameters.GetQueryParameter("lat", "" + (layoutToUse?.startLat ?? 0), "The initial/current latitude")
@ -151,6 +152,7 @@ export default class State {
});
}
// Helper function to initialize feature switches
function featSw(key: string, deflt: (layout: LayoutConfig) => boolean, documentation: string): UIEventSource<boolean> {
const queryParameterSource = QueryParameters.GetQueryParameter(key, undefined, documentation);
@ -211,11 +213,12 @@ export default class State {
);
this.installedThemes = new InstalledThemes(this.osmConnection).installedThemes;
// Important: the favourite layers are initialized _after_ the installed themes, as these might contain an installedTheme
this.favouriteLayers = this.osmConnection.GetLongPreference("favouriteLayers").map(
this.favouriteLayers = LocalStorageSource.Get("favouriteLayers")
.syncWith(this.osmConnection.GetLongPreference("favouriteLayers"))
.map(
str => Utils.Dedup(str?.split(";")) ?? [],
[], layers => Utils.Dedup(layers)?.join(";")
);

View file

@ -21,7 +21,6 @@ export default class ScrollableFullScreen extends UIElement {
Svg.close_svg().SetClass("hidden md:block")
])
.onClick(() => {
console.log("Closing...")
ScrollableFullScreen.RestoreLeaflet();
if (onClose !== undefined) {
onClose();

View file

@ -22,7 +22,7 @@ export default class LayerControlPanel extends UIElement {
}
if (State.state.filteredLayers.data.length > 1) {
const layerSelection = new LayerSelection();
const layerSelection = new LayerSelection(State.state.filteredLayers);
layerSelection.onClick(() => {
});
layerControlPanel = new Combine([layerSelection, "<br/>", layerControlPanel]);

View file

@ -6,19 +6,36 @@ import CheckBox from "../Input/CheckBox";
import Combine from "../Base/Combine";
import {FixedUiElement} from "../Base/FixedUiElement";
import Translations from "../i18n/Translations";
import LayerConfig from "../../Customizations/JSON/LayerConfig";
/**
* Shows the panel with all layers and a toggle for each of them
*/
export default class LayerSelection extends UIElement {
private readonly _checkboxes: UIElement[];
private _checkboxes: UIElement[];
private activeLayers: UIEventSource<{
readonly isDisplayed: UIEventSource<boolean>,
readonly layerDef: LayerConfig;
}[]>;
constructor(activeLayers: UIEventSource<{
readonly isDisplayed: UIEventSource<boolean>,
readonly layerDef: LayerConfig;
}[]>) {
super(activeLayers);
if(activeLayers === undefined){
throw "ActiveLayers should be defined..."
}
this.activeLayers = activeLayers;
}
InnerRender(): string {
constructor() {
super(undefined);
this._checkboxes = [];
for (const layer of State.state.filteredLayers.data) {
for (const layer of this.activeLayers.data) {
const leafletStyle = layer.layerDef.GenerateLeafletStyle(
new UIEventSource<any>({id: "node/-1"}),
false)
@ -54,9 +71,8 @@ export default class LayerSelection extends UIElement {
.SetStyle("margin:0.3em;")
);
}
}
InnerRender(): string {
return new Combine(this._checkboxes)
.SetStyle("display:flex;flex-direction:column;")
.Render();

View file

@ -8,7 +8,7 @@ export default class CenterMessageBox extends UIElement {
super(State.state.centerMessage);
this.ListenTo(State.state.locationControl);
this.ListenTo(State.state.layerUpdater.retries);
this.ListenTo(State.state.layerUpdater.timeout);
this.ListenTo(State.state.layerUpdater.runningQuery);
this.ListenTo(State.state.layerUpdater.sufficientlyZoomed);
}
@ -18,9 +18,9 @@ export default class CenterMessageBox extends UIElement {
return {innerHtml: State.state.centerMessage.data, done: false};
}
const lu = State.state.layerUpdater;
if (lu.retries.data > 0) {
if (lu.timeout.data > 0) {
return {
innerHtml: Translations.t.centerMessage.retrying.Subs({count: "" + lu.retries.data}).Render(),
innerHtml: Translations.t.centerMessage.retrying.Subs({count: "" + lu.timeout.data}).Render(),
done: false
};
}

View file

@ -102,6 +102,10 @@ export default class TagRenderingQuestion extends UIElement {
return ff;
}
if(ff){
mappings.push(ff);
}
if (this._configuration.multiAnswer) {
return this.GenerateMultiAnswer(mappings, ff)
} else {

View file

@ -15,7 +15,7 @@ import {GeoOperations} from "../Logic/GeoOperations";
export default class ShowDataLayer {
private readonly _layerDict;
private _layerDict;
private readonly _leafletMap: UIEventSource<L.Map>;
constructor(features: UIEventSource<{ feature: any, freshness: Date }[]>,
@ -24,12 +24,11 @@ export default class ShowDataLayer {
this._leafletMap = leafletMap;
const self = this;
const mp = leafletMap.data;
this._layerDict = {};
self._layerDict = {};
layoutToUse.addCallbackAndRun(layoutToUse => {
for (const layer of layoutToUse.layers) {
this._layerDict[layer.id] = layer;
self._layerDict[layer.id] = layer;
}
});
@ -81,7 +80,7 @@ export default class ShowDataLayer {
const tagsSource = State.state.allElements.getEventSourceFor(feature);
// Every object is tied to exactly one layer
const layer = this._layerDict[feature._matching_layer_id];
return layer.GenerateLeafletStyle(tagsSource, layer._showOnPopup !== undefined);
return layer?.GenerateLeafletStyle(tagsSource, layer._showOnPopup !== undefined);
}
private pointToLayer(feature, latLng): L.Layer {
@ -111,6 +110,10 @@ export default class ShowDataLayer {
private postProcessFeature(feature, leafletLayer: L.Layer) {
const layer: LayerConfig = this._layerDict[feature._matching_layer_id];
if(layer === undefined){
console.warn("No layer found for object (probably a now disabled layer)", feature)
return;
}
if (layer.title === undefined && (layer.tagRenderings ?? []).length === 0) {
// No popup action defined -> Don't do anything
return;
@ -159,10 +162,10 @@ export default class ShowDataLayer {
const mp = this._leafletMap.data;
if (!popup.isOpen() && mp !== undefined) {
var centerpoint = GeoOperations.centerpointCoordinates(feature);
popup
.setLatLng(GeoOperations.centerpointCoordinates(feature))
.openOn(mp);
uiElement.Activate();
}
}
}

View file

@ -1,6 +1,4 @@
import * as $ from "jquery"
import Constants from "./Models/Constants";
export class Utils {

View file

@ -9,7 +9,7 @@
"scripts": {
"increase-memory": "export NODE_OPTIONS=--max_old_space_size=4096",
"start": "npm run increase-memory && parcel *.html UI/** Logic/** assets/** assets/**/** assets/**/**/** vendor/* vendor/*/*",
"test": "ts-node test/*",
"test": "ts-node test/Tag.spec.ts && ts-node test/TagQuestion.spec.ts",
"generate:editor-layer-index": "cd assets/ && wget https://osmlab.github.io/editor-layer-index/imagery.geojson --output-document=editor-layer-index.json",
"generate:images": "ts-node scripts/generateIncludedImages.ts",
"generate:translations": "ts-node scripts/generateTranslations.ts",

View file

@ -15,7 +15,7 @@ import PublicHolidayInput from "../UI/OpeningHours/PublicHolidayInput";
import {SubstitutedTranslation} from "../UI/SubstitutedTranslation";
new T([
new T("Tags", [
["Tag replacement works in translation", () => {
const tr = new Translation({
"en": "Test {key} abc"

60
test/TagQuestion.spec.ts Normal file
View file

@ -0,0 +1,60 @@
import T from "./TestHelper";
import {Utils} from "../Utils";
Utils.runningFromConsole = true;
import TagRenderingQuestion from "../UI/Popup/TagRenderingQuestion";
import {UIEventSource} from "../Logic/UIEventSource";
import TagRenderingConfig from "../Customizations/JSON/TagRenderingConfig";
import {equal} from "assert";
import * as assert from "assert";
new T("TagQuestionElement",
[
["Freeform has textfield", () => {
const tags = new UIEventSource({
id: "way/123",
amenity: 'public_bookcases'
});
const config = new TagRenderingConfig(
{
render: "The name is {name}",
question: "What is the name of this bookcase?",
freeform: {
key: "name",
type: "string"
}
}, undefined, "Testing tag"
);
const questionElement = new TagRenderingQuestion(tags, config);
const html = questionElement.InnerRender();
T.assertContains("What is the name of this bookcase?", html);
T.assertContains("<input type='text'", html);
}],
["TagsQuestion with Freeform and mappings has textfield", () => {
const tags = new UIEventSource({
id: "way/123",
amenity: 'public_bookcases'
});
const config = new TagRenderingConfig(
{
render: "The name is {name}",
question: "What is the name of this bookcase?",
freeform: {
key: "name",
type: "string"
},
mappings: [
{"if": "noname=yes",
"then": "This bookcase has no name"}
]
}, undefined, "Testing tag"
);
const questionElement = new TagRenderingQuestion(tags, config);
const html = questionElement.InnerRender();
T.assertContains("What is the name of this bookcase?", html);
T.assertContains("This bookcase has no name", html);
T.assertContains("<input type='text'", html);
}]
]
);

View file

@ -1,6 +1,7 @@
export default class T {
constructor(tests: [string, () => void ][]) {
constructor(testsuite: string, tests: [string, () => void ][]) {
let failures : string []= [];
for (const [name, test] of tests) {
try {
@ -11,11 +12,17 @@ export default class T {
}
}
if (failures.length == 0) {
console.log("All tests done!")
console.log(`All tests of ${testsuite} done!`)
} else {
console.warn(failures.length, "tests failed :(")
console.warn(failures.length, `tests of ${testsuite} failed :(`)
console.log("Failed tests: ", failures.join(","))
}
}
static assertContains(needle: string, actual: string){
if(actual.indexOf(needle) < 0){
throw `The substring ${needle} was not found`
}
}
}