diff --git a/umap/static/umap/js/modules/global.js b/umap/static/umap/js/modules/global.js index f42ce016..49f2822b 100644 --- a/umap/static/umap/js/modules/global.js +++ b/umap/static/umap/js/modules/global.js @@ -5,6 +5,7 @@ import Caption from './caption.js' import { Panel, EditPanel, FullPanel } from './ui/panel.js' import Dialog from './ui/dialog.js' import Tooltip from './ui/tooltip.js' +import Rules from './rules.js' import * as Utils from './utils.js' import { SCHEMA } from './schema.js' import { Request, ServerRequest, RequestError, HTTPError, NOKError } from './request.js' @@ -43,6 +44,7 @@ window.U = { Panel, Request, RequestError, + Rules, SCHEMA, ServerRequest, SyncEngine, diff --git a/umap/static/umap/js/modules/rules.js b/umap/static/umap/js/modules/rules.js new file mode 100644 index 00000000..0c60dbe1 --- /dev/null +++ b/umap/static/umap/js/modules/rules.js @@ -0,0 +1,175 @@ +import { DomUtil, DomEvent, stamp } from '../../vendors/leaflet/leaflet-src.esm.js' +import * as Utils from './utils.js' +import { translate } from './i18n.js' + +class Rule { + constructor(map, condition = '', options = {}) { + this.map = map + this.condition = condition + this.parse() + this.options = options + let isDirty = false + Object.defineProperty(this, 'isDirty', { + get: () => { + return isDirty + }, + set: (status) => { + isDirty = status + if (status) { + this.map.isDirty = status + } + }, + }) + } + + render(fields) { + this.map.render(fields) + } + + parse() { + let vars = [] + if (this.condition.includes('!=')) { + this.operator = (our, other) => our != other + vars = this.condition.split('!=') + } else if (this.condition.includes('=')) { + this.operator = (our, other) => our === other + vars = this.condition.split('=') + } + if (vars.length != 2) this.operator = undefined + this.key = vars[0] + this.expected = vars[1] + } + + match(props) { + if (!this.operator) return false + return this.operator(this.expected, props[this.key]) + } + + getMap() { + return this.map + } + + getOption(option) { + return this.options[option] + } + + edit() { + const options = [ + [ + 'condition', + { + handler: 'BlurInput', + label: L._('Condition'), + placeholder: translate('key=value or key!=value'), + }, + ], + 'options.color', + 'options.iconClass', + 'options.iconUrl', + 'options.iconOpacity', + 'options.opacity', + 'options.weight', + 'options.fill', + 'options.fillColor', + 'options.fillOpacity', + 'options.smoothFactor', + 'options.dashArray', + ] + const container = DomUtil.create('div') + const builder = new U.FormBuilder(this, options) + const defaultShapeProperties = L.DomUtil.add('div', '', container) + defaultShapeProperties.appendChild(builder.build()) + + this.map.editPanel.open({ content: container }) + } + + renderToolbox(container) { + const toggle = L.DomUtil.createButtonIcon( + container, + 'icon-eye', + L._('Show/hide layer') + ) + const edit = L.DomUtil.createButtonIcon( + container, + 'icon-edit show-on-edit', + L._('Edit') + ) + const remove = L.DomUtil.createButtonIcon( + container, + 'icon-delete show-on-edit', + L._('Delete layer') + ) + L.DomEvent.on(edit, 'click', this.edit, this) + L.DomEvent.on( + remove, + 'click', + function () { + if (!confirm(L._('Are you sure you want to delete this rule?'))) return + this._delete() + this.map.editPanel.close() + }, + this + ) + DomUtil.add('span', '', container, this.condition || translate('empty rule')) + //L.DomEvent.on(toggle, 'click', this.toggle, this) + } + + _delete() { + this.map.rules.rules = this.map.rules.rules.filter((rule) => rule != this) + } +} + +export default class Rules { + constructor(map) { + this.map = map + this.rules = [] + this.loadRules() + } + + loadRules() { + if (!this.map.options.rules?.length) return + for (const { condition, options } of this.map.options.rules) { + if (!condition) continue + this.rules.push(new Rule(this.map, condition, options)) + } + } + + edit(container) { + const body = L.DomUtil.createFieldset( + container, + translate('Conditional style rules') + ) + if (this.rules.length) { + const list = DomUtil.create('ul', '', body) + for (const rule of this.rules) { + rule.renderToolbox(DomUtil.create('li', '', list)) + } + } + L.DomUtil.createButton('umap-add', body, translate('Add rule'), this.addRule, this) + } + + addRule() { + const rule = new Rule(this.map) + rule.isDirty = true + this.rules.push(rule) + rule.edit(map) + } + + commit() { + this.map.options.rules = this.rules.map((rule) => { + return { + condition: rule.condition, + options: rule.options, + } + }) + } + + getOption(option, feature) { + for (const rule of this.rules) { + if (rule.match(feature.properties)) { + if (Utils.usableOption(rule.options, option)) return rule.options[option] + break + } + } + } +} diff --git a/umap/static/umap/js/modules/schema.js b/umap/static/umap/js/modules/schema.js index 07e520bb..694a4512 100644 --- a/umap/static/umap/js/modules/schema.js +++ b/umap/static/umap/js/modules/schema.js @@ -386,6 +386,10 @@ export const SCHEMA = { type: Object, impacts: ['remote-data'], }, + rules: { + type: Object, + impacts: ['data'], + }, scaleControl: { type: Boolean, impacts: ['ui'], diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js index 9cc41025..13702036 100644 --- a/umap/static/umap/js/umap.js +++ b/umap/static/umap/js/umap.js @@ -419,6 +419,7 @@ U.Map = L.Map.extend({ this.importer = new U.Importer(this) this.drop = new U.DropControl(this) this.share = new U.Share(this) + this.rules = new U.Rules(this) this._controls.tilelayers = new U.TileLayerControl(this) }, @@ -823,7 +824,15 @@ U.Map = L.Map.extend({ return U.SCHEMA[option] && U.SCHEMA[option].default }, - getOption: function (option) { + getRuleOption: function (option, feature) { + return this.rules.getOption(option, feature) + }, + + getOption: function (option, feature) { + if (feature) { + const value = this.getRuleOption(option, feature) + if (value !== undefined) return value + } if (U.Utils.usableOption(this.options, option)) return this.options[option] return this.getDefaultOption(option) }, @@ -1031,6 +1040,7 @@ U.Map = L.Map.extend({ }, saveSelf: async function () { + this.rules.commit() const geojson = { type: 'Feature', geometry: this.geometry(), @@ -1561,6 +1571,7 @@ U.Map = L.Map.extend({ this._editShapeProperties(container) this._editDefaultProperties(container) this._editInteractionsProperties(container) + this.rules.edit(container) this._editTilelayer(container) this._editOverlay(container) this._editBounds(container) diff --git a/umap/static/umap/js/umap.layer.js b/umap/static/umap/js/umap.layer.js index faf2d5d4..177f278a 100644 --- a/umap/static/umap/js/umap.layer.js +++ b/umap/static/umap/js/umap.layer.js @@ -1496,7 +1496,7 @@ U.DataLayer = L.Evented.extend({ } else if (this.layer && this.layer.defaults && this.layer.defaults[option]) { return this.layer.defaults[option] } else { - return this.map.getOption(option) + return this.map.getOption(option, feature) } },