diff --git a/umap/static/umap/js/modules/data/features.js b/umap/static/umap/js/modules/data/features.js index e15b4924..4a1daf4c 100644 --- a/umap/static/umap/js/modules/data/features.js +++ b/umap/static/umap/js/modules/data/features.js @@ -118,6 +118,10 @@ class Feature { this._ui = new klass(this, this.toLatLngs()) } + getUIClass() { + return this.getOption('UIClass') + } + getClassName() { return this.staticOptions.className } @@ -556,8 +560,8 @@ class Feature { properties.lon = center.lng properties.lng = center.lng properties.alt = center?.alt - if (typeof this.getMeasure !== 'undefined') { - properties.measure = this.getMeasure() + if (typeof this.ui.getMeasure !== 'undefined') { + properties.measure = this.ui.getMeasure() } } return L.extend(properties, this.properties) @@ -598,7 +602,7 @@ export class Point extends Feature { } getUIClass() { - return LeafletMarker + return super.getUIClass() || LeafletMarker } hasGeom() { @@ -690,21 +694,6 @@ class Path extends Feature { L.DomEvent.stop(event) } - getStyleOptions() { - return [ - 'smoothFactor', - 'color', - 'opacity', - 'stroke', - 'weight', - 'fill', - 'fillColor', - 'fillOpacity', - 'dashArray', - 'interactive', - ] - } - getShapeOptions() { return [ 'properties._umap_options.color', @@ -721,16 +710,6 @@ class Path extends Feature { ] } - getStyle() { - const options = {} - for (const option of this.getStyleOptions()) { - options[option] = this.getDynamicOption(option) - } - if (options.interactive) options.pointerEvents = 'visiblePainted' - else options.pointerEvents = 'stroke' - return options - } - getBestZoom() { return this.getOption('zoomTo') || this.map.getBoundsZoom(this.bounds, true) } @@ -817,18 +796,13 @@ export class LineString extends Path { } getUIClass() { - return LeafletPolyline + return super.getUIClass() || LeafletPolyline } isSameClass(other) { return other instanceof LineString } - getMeasure(shape) { - const length = L.GeoUtil.lineLength(this.map, shape || this.ui._defaultShape()) - return L.GeoUtil.readableDistance(length, this.map.measureTools.getMeasureUnit()) - } - toPolygon() { const geojson = this.toGeoJSON() geojson.geometry.type = 'Polygon' @@ -935,7 +909,7 @@ export class Polygon extends Path { getUIClass() { if (this.getOption('mask')) return MaskPolygon - return LeafletPolygon + return super.getUIClass() || LeafletPolygon } isSameClass(other) { @@ -967,11 +941,6 @@ export class Polygon extends Path { return options } - getMeasure(shape) { - const area = L.GeoUtil.geodesicArea(shape || this.ui._defaultShape()) - return L.GeoUtil.readableArea(area, this.map.measureTools.getMeasureUnit()) - } - toLineString() { const geojson = this.toGeoJSON() delete geojson.id diff --git a/umap/static/umap/js/modules/data/layer.js b/umap/static/umap/js/modules/data/layer.js index 915fe5ba..151eee46 100644 --- a/umap/static/umap/js/modules/data/layer.js +++ b/umap/static/umap/js/modules/data/layer.js @@ -11,7 +11,7 @@ import * as Utils from '../utils.js' import { Default as DefaultLayer } from '../rendering/layers/base.js' import { Cluster } from '../rendering/layers/cluster.js' import { Heat } from '../rendering/layers/heat.js' -import { Categorized, Choropleth } from '../rendering/layers/relative.js' +import { Categorized, Choropleth, Circles } from '../rendering/layers/classified.js' import { uMapAlert as Alert, uMapAlertConflict as AlertConflict, @@ -21,7 +21,14 @@ import { DataLayerPermissions } from '../permissions.js' import { Point, LineString, Polygon } from './features.js' import TableEditor from '../tableeditor.js' -export const LAYER_TYPES = [DefaultLayer, Cluster, Heat, Choropleth, Categorized] +export const LAYER_TYPES = [ + DefaultLayer, + Cluster, + Heat, + Choropleth, + Categorized, + Circles, +] const LAYER_MAP = LAYER_TYPES.reduce((acc, klass) => { acc[klass.TYPE] = klass @@ -190,6 +197,8 @@ export class DataLayer { if (visible) this.map.removeLayer(this.layer) const Class = LAYER_MAP[this.options.type] || DefaultLayer this.layer = new Class(this) + // Rendering layer changed, so let's force reset the feature rendering too. + this.eachFeature((feature) => feature.makeUI()) this.eachFeature(this.showFeature) if (visible) this.show() this.propagateRemote() diff --git a/umap/static/umap/js/modules/rendering/layers/relative.js b/umap/static/umap/js/modules/rendering/layers/classified.js similarity index 73% rename from umap/static/umap/js/modules/rendering/layers/relative.js rename to umap/static/umap/js/modules/rendering/layers/classified.js index 010d76a1..42c3067a 100644 --- a/umap/static/umap/js/modules/rendering/layers/relative.js +++ b/umap/static/umap/js/modules/rendering/layers/classified.js @@ -2,11 +2,12 @@ import { FeatureGroup, DomUtil } from '../../../../vendors/leaflet/leaflet-src.e import { translate } from '../../i18n.js' import { LayerMixin } from './base.js' import * as Utils from '../../utils.js' +import { CircleMarker } from '../ui.js' // Layer where each feature color is relative to the others, // so we need all features before behing able to set one // feature layer -const RelativeColorLayerMixin = { +const ClassifiedMixin = { initialize: function (datalayer) { this.datalayer = datalayer this.colorSchemes = Object.keys(colorbrewer) @@ -16,10 +17,13 @@ const RelativeColorLayerMixin = { if (!Utils.isObject(this.datalayer.options[key])) { this.datalayer.options[key] = {} } + this.ensureOptions(this.datalayer.options[key]) FeatureGroup.prototype.initialize.call(this, [], this.datalayer.options[key]) LayerMixin.onInit.call(this, this.datalayer.map) }, + ensureOptions: () => {}, + dataChanged: function () { this.redraw() }, @@ -29,9 +33,15 @@ const RelativeColorLayerMixin = { if (this._map) this.eachLayer(this._map.addLayer, this._map) }, + getStyleProperty: (feature) => { + return feature.staticOptions.mainColor + }, + getOption: function (option, feature) { - if (feature && option === feature.staticOptions.mainColor) { - return this.getColor(feature) + if (!feature) return + if (option === this.getStyleProperty(feature)) { + const value = this._getOption(feature) + return value } }, @@ -85,11 +95,10 @@ export const Choropleth = FeatureGroup.extend({ NAME: translate('Choropleth'), TYPE: 'Choropleth', }, - includes: [LayerMixin, RelativeColorLayerMixin], + includes: [LayerMixin, ClassifiedMixin], // Have defaults that better suit the choropleth mode. defaults: { color: 'white', - fillColor: 'red', fillOpacity: 0.7, weight: 2, }, @@ -142,13 +151,12 @@ export const Choropleth = FeatureGroup.extend({ this.datalayer.options.choropleth.breaks = this.options.breaks .map((b) => +b.toFixed(2)) .join(',') - const fillColor = this.datalayer.getOption('fillColor') || this.defaults.fillColor let colorScheme = this.datalayer.options.choropleth.brewer if (!colorbrewer[colorScheme]) colorScheme = 'Blues' this.options.colors = colorbrewer[colorScheme][this.options.breaks.length - 1] || [] }, - getColor: function (feature) { + _getOption: function (feature) { if (!feature) return // FIXME should not happen const featureValue = this._getValue(feature) // Find the bucket/step/limit that this value is less than and give it that color @@ -235,19 +243,132 @@ export const Choropleth = FeatureGroup.extend({ }, }) +export const Circles = FeatureGroup.extend({ + statics: { + NAME: translate('Proportional circles'), + TYPE: 'Circles', + }, + includes: [LayerMixin, ClassifiedMixin], + defaults: { + weight: 1, + UIClass: CircleMarker, + }, + + ensureOptions: function (options) { + if (!Utils.isObject(this.datalayer.options.circles.radius)) { + this.datalayer.options.circles.radius = {} + } + }, + + _getValue: function (feature) { + const key = this.datalayer.options.circles.property || 'value' + const value = +feature.properties[key] + if (!Number.isNaN(value)) return value + }, + + compute: function () { + const values = this.getValues() + this.options.minValue = Math.sqrt(Math.min(...values)) + this.options.maxValue = Math.sqrt(Math.max(...values)) + this.options.minPX = this.datalayer.options.circles.radius?.min || 2 + this.options.maxPX = this.datalayer.options.circles.radius?.max || 50 + }, + + onEdit: function (field, builder) { + this.compute() + }, + + _computeRadius: function (value) { + const valuesRange = this.options.maxValue - this.options.minValue + const pxRange = this.options.maxPX - this.options.minPX + const radius = + this.options.minPX + + ((Math.sqrt(value) - this.options.minValue) / valuesRange) * pxRange + return radius || this.options.minPX + }, + + _getOption: function (feature) { + if (!feature) return // FIXME should not happen + return this._computeRadius(this._getValue(feature)) + }, + + getEditableOptions: function () { + return [ + [ + 'options.circles.property', + { + handler: 'Select', + selectOptions: this.datalayer._propertiesIndex, + label: translate('Property name to compute circles'), + }, + ], + [ + 'options.circles.radius.min', + { + handler: 'Range', + label: translate('Min circle radius'), + min: 2, + max: 10, + step: 1, + }, + ], + [ + 'options.circles.radius.max', + { + handler: 'Range', + label: translate('Max circle radius'), + min: 12, + max: 50, + step: 2, + }, + ], + ] + }, + + getStyleProperty: (feature) => { + return 'radius' + }, + + renderLegend: function (container) { + const parent = DomUtil.create('ul', 'circles-layer-legend', container) + const color = this.datalayer.getOption('color') + const values = this.getValues() + if (!values.length) return + values.sort((a, b) => a - b) + const minValue = values[0] + const maxValue = values[values.length - 1] + const medianValue = values[Math.round(values.length / 2)] + const items = [ + [this.options.minPX, minValue], + [this._computeRadius(medianValue), medianValue], + [this.options.maxPX, maxValue], + ] + for (const [size, label] of items) { + const li = DomUtil.create('li', '', parent) + const circleEl = DomUtil.create('span', 'circle', li) + circleEl.style.backgroundColor = color + circleEl.style.height = `${size * 2}px` + circleEl.style.width = `${size * 2}px` + circleEl.style.opacity = this.datalayer.getOption('opacity') + const labelEl = DomUtil.create('span', 'label', li) + labelEl.textContent = label + } + }, +}) + export const Categorized = FeatureGroup.extend({ statics: { NAME: translate('Categorized'), TYPE: 'Categorized', }, - includes: [LayerMixin, RelativeColorLayerMixin], + includes: [LayerMixin, ClassifiedMixin], MODES: { manual: translate('Manual'), alpha: translate('Alphabetical'), }, defaults: { color: 'white', - fillColor: 'red', + // fillColor: 'red', fillOpacity: 0.7, weight: 2, }, @@ -258,7 +379,7 @@ export const Categorized = FeatureGroup.extend({ return feature.properties[key] }, - getColor: function (feature) { + _getOption: function (feature) { if (!feature) return // FIXME should not happen const featureValue = this._getValue(feature) for (let i = 0; i < this.options.categories.length; i++) { @@ -290,7 +411,6 @@ export const Categorized = FeatureGroup.extend({ } this.options.categories = categories this.datalayer.options.categorized.categories = this.options.categories.join(',') - const fillColor = this.datalayer.getOption('fillColor') || this.defaults.fillColor const colorScheme = this.datalayer.options.categorized.brewer this._classes = this.options.categories.length if (colorbrewer[colorScheme]?.[this._classes]) { diff --git a/umap/static/umap/js/modules/rendering/ui.js b/umap/static/umap/js/modules/rendering/ui.js index f86888c6..a1ce47c9 100644 --- a/umap/static/umap/js/modules/rendering/ui.js +++ b/umap/static/umap/js/modules/rendering/ui.js @@ -3,9 +3,11 @@ import { Marker, Polyline, Polygon, + CircleMarker as BaseCircleMarker, DomUtil, LineUtil, latLng, + LatLng, LatLngBounds, } from '../../../vendors/leaflet/leaflet-src.esm.js' import { translate } from '../i18n.js' @@ -266,7 +268,7 @@ export const LeafletMarker = Marker.extend({ const PathMixin = { _onMouseOver: function () { if (this._map.measureTools?.enabled()) { - this._map.tooltip.open({ content: this.feature.getMeasure(), anchor: this }) + this._map.tooltip.open({ content: this.getMeasure(), anchor: this }) } else if (this._map.editEnabled && !this._map.editedFeature) { this._map.tooltip.open({ content: translate('Click to edit'), anchor: this }) } @@ -295,8 +297,8 @@ const PathMixin = { onAdd: function (map) { this._container = null - this.setStyle() FeatureMixin.onAdd.call(this, map) + this.setStyle() if (this.editing?.enabled()) this.editing.addHooks() this.resetTooltip() this._path.dataset.feature = this.feature.id @@ -308,7 +310,7 @@ const PathMixin = { }, setStyle: function (options = {}) { - for (const option of this.feature.getStyleOptions()) { + for (const option of this.getStyleOptions()) { options[option] = this.feature.getDynamicOption(option) } options.pointerEvents = options.interactive ? 'visiblePainted' : 'stroke' @@ -333,7 +335,7 @@ const PathMixin = { let items = FeatureMixin.getContextMenuItems.call(this, event) items.push({ text: translate('Display measure'), - callback: () => Alert.info(this.feature.getMeasure()), + callback: () => Alert.info(this.getMeasure()), }) if (this._map.editEnabled && !this.feature.isReadOnly() && this.feature.isMulti()) { items = items.concat(this.getContextMenuMultiItems(event)) @@ -396,6 +398,19 @@ const PathMixin = { if (!shape) return return this.feature.isolateShape(shape) }, + + getStyleOptions: () => [ + 'smoothFactor', + 'color', + 'opacity', + 'stroke', + 'weight', + 'fill', + 'fillColor', + 'fillOpacity', + 'dashArray', + 'interactive', + ], } export const LeafletPolyline = Polyline.extend({ @@ -454,6 +469,12 @@ export const LeafletPolyline = Polyline.extend({ }) return items }, + + getMeasure: function (shape) { + // FIXME: compute from data in feature (with TurfJS) + const length = L.GeoUtil.lineLength(this._map, shape || this._defaultShape()) + return L.GeoUtil.readableDistance(length, this._map.measureTools.getMeasureUnit()) + }, }) export const LeafletPolygon = Polygon.extend({ @@ -488,6 +509,11 @@ export const LeafletPolygon = Polygon.extend({ startHole: function (event) { this.enableEdit().newHole(event.latlng) }, + + getMeasure: function (shape) { + const area = L.GeoUtil.geodesicArea(shape || this._defaultShape()) + return L.GeoUtil.readableArea(area, this._map.measureTools.getMeasureUnit()) + }, }) const WORLD = [ latLng([90, 180]), @@ -523,3 +549,25 @@ export const MaskPolygon = LeafletPolygon.extend({ return this._latlngs[1] }, }) + +export const CircleMarker = BaseCircleMarker.extend({ + parentClass: BaseCircleMarker, + includes: [FeatureMixin, PathMixin], + initialize: function (feature, latlng) { + if (Array.isArray(latlng) && !(latlng[0] instanceof Number)) { + // Must be a line or polygon + const bounds = new LatLngBounds(latlng) + latlng = bounds.getCenter() + } + FeatureMixin.initialize.call(this, feature, latlng) + }, + getClass: () => CircleMarker, + getStyleOptions: function () { + const options = PathMixin.getStyleOptions.call(this) + options.push('radius') + return options + }, + getCenter: function () { + return this._latlng + }, +}) diff --git a/umap/static/umap/js/modules/schema.js b/umap/static/umap/js/modules/schema.js index 363b56c4..233c6742 100644 --- a/umap/static/umap/js/modules/schema.js +++ b/umap/static/umap/js/modules/schema.js @@ -57,6 +57,10 @@ export const SCHEMA = { type: Object, impacts: ['data'], }, + circles: { + type: Object, + impacts: ['data'], + }, cluster: { type: Object, impacts: ['data'], diff --git a/umap/static/umap/js/umap.controls.js b/umap/static/umap/js/umap.controls.js index 3423f330..7ec574b3 100644 --- a/umap/static/umap/js/umap.controls.js +++ b/umap/static/umap/js/umap.controls.js @@ -1261,7 +1261,7 @@ U.Editable = L.Editable.extend({ } else { const tmpLatLngs = e.layer.editor._drawnLatLngs.slice() tmpLatLngs.push(e.latlng) - measure = e.layer.feature.getMeasure(tmpLatLngs) + measure = e.layer.getMeasure(tmpLatLngs) if (e.layer.editor._drawnLatLngs.length < e.layer.editor.MIN_VERTEX) { // when drawing second point @@ -1273,7 +1273,7 @@ U.Editable = L.Editable.extend({ } } else { // when moving an existing point - measure = e.layer.feature.getMeasure() + measure = e.layer.getMeasure() } if (measure) { if (e.layer instanceof L.Polygon) { diff --git a/umap/static/umap/map.css b/umap/static/umap/map.css index dbadbf09..9632b74d 100644 --- a/umap/static/umap/map.css +++ b/umap/static/umap/map.css @@ -994,6 +994,19 @@ a.umap-control-caption, padding: 0; margin: 0; } +.datalayer-legend .circles-layer-legend { + padding: var(--box-padding); +} +.circles-layer-legend li { + display: flex; + align-items: center; + justify-content: space-between; +} +.circles-layer-legend li .circle { + border-radius: 50%; + display: inline-block; + text-align: center; +} /* ********************************* */ /* Popup */ diff --git a/umap/tests/fixtures/test_circles_layer.geojson b/umap/tests/fixtures/test_circles_layer.geojson new file mode 100644 index 00000000..d4ff092b --- /dev/null +++ b/umap/tests/fixtures/test_circles_layer.geojson @@ -0,0 +1,219 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -1.5869228, + 47.1988448 + ] + }, + "properties": { + "@id": "node/8371195387", + "access": "private", + "amenity": "bicycle_parking", + "covered": "yes", + "fee": "no", + "name": "station with unknown capacity" + }, + "id": "capa0" + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -1.5567325, + 47.2223201 + ] + }, + "properties": { + "@id": "node/3750335624", + "amenity": "bicycle_parking", + "bicycle_parking": "stands", + "capacity": "2", + "check_date:capacity": "2021-05-12", + "covered": "no", + "material": "metal", + "name": "tiny station with 2" + }, + "id": "capa2" + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -1.5293571, + 47.2285598 + ] + }, + "properties": { + "@id": "node/7149702104", + "amenity": "bicycle_parking", + "bicycle_parking": "wall_loops", + "covered": "yes", + "capacity": "2", + "name": "tiny station with 3" + }, + "id": "capa3" + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -1.5613176, + 47.2223468 + ] + }, + "properties": { + "@id": "node/5206704322", + "amenity": "bicycle_parking", + "bicycle_parking": "stands", + "capacity": "4", + "name": "small station with 4" + }, + "id": "capa4" + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -1.5465724, + 47.2074831 + ] + }, + "properties": { + "@id": "node/10539987424", + "amenity": "bicycle_parking", + "bicycle_parking": "stands", + "capacity": "6", + "covered": "no", + "material": "metal", + "name": "small station with 6" + }, + "id": "capa6" + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -1.5877882, + 47.2179441 + ] + }, + "properties": { + "@id": "node/10239046793", + "amenity": "bicycle_parking", + "capacity": 8 + }, + "id": "capa8" + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -1.5406938, + 47.2312451 + ] + }, + "properties": { + "@id": "node/5176046494", + "amenity": "bicycle_parking", + "bicycle_parking": "rack", + "capacity": "27", + "name": "middle station with 27" + }, + "id": "cap27" + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -1.5344658, + 47.1988441 + ] + }, + "properties": { + "@id": "node/4126326089", + "access": "private", + "amenity": "bicycle_parking", + "bicycle_parking": "building", + "capacity": "64", + "covered": "yes", + "material": "metal", + "name": "middle station with 64" + }, + "id": "cap64" + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -1.5433176, + 47.2172633 + ] + }, + "properties": { + "@id": "way/1001063092", + "access": "yes", + "amenity": "bicycle_parking", + "architect": "Forma 6;Phytolab", + "bicycle_parking": "shed", + "building": "roof", + "building:architecture": "contemporary", + "capacity": "676", + "capacity:cargo_bike": "22", + "capacity:motorcycle": "33", + "covered": "yes", + "fee": "no", + "name": "big station with 676", + "start_date": "2021-11-15", + "@geometry": "center" + }, + "id": "ca676" + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -1.5291998, + 47.259166 + ] + }, + "properties": { + "@id": "node/11141117088", + "amenity": "bicycle_parking", + "bicycle_parking": "stands", + "capacity": "1160", + "temporary": "yes", + "temporary:date_off": "2023-10-28", + "temporary:date_on": "2023-09-08", + "name": "huge station with 1160" + }, + "id": "c1160" + } + ], + "_umap_options": { + "displayOnLoad": true, + "inCaption": true, + "browsable": true, + "editMode": "advanced", + "name": "Calque 1", + "remoteData": {}, + "type": "Circles", + "circles": { + "radius": {"min": 2, "max": 40}, + "property": "capacity" + } + } +} diff --git a/umap/tests/integration/test_circles_layer.py b/umap/tests/integration/test_circles_layer.py new file mode 100644 index 00000000..a8663f2c --- /dev/null +++ b/umap/tests/integration/test_circles_layer.py @@ -0,0 +1,69 @@ +import json +from pathlib import Path + +import pytest +from playwright.sync_api import expect + +from ..base import DataLayerFactory + +pytestmark = pytest.mark.django_db + + +def test_basic_circles_layer(map, live_server, page): + path = Path(__file__).parent.parent / "fixtures/test_circles_layer.geojson" + data = json.loads(path.read_text()) + DataLayerFactory(data=data, map=map) + page.goto(f"{live_server.url}{map.get_absolute_url()}#12/47.2210/-1.5621") + paths = page.locator("path") + expect(paths).to_have_count(10) + # Last arc curve command + assert ( + page.locator("[data-feature=c1160]") + .get_attribute("d") + .endswith("a40,40 0 1,0 -80,0 ") + ) + assert ( + page.locator("[data-feature=ca676]") + .get_attribute("d") + .endswith("a31,31 0 1,0 -62,0 ") + ) + assert ( + page.locator("[data-feature=cap64]") + .get_attribute("d") + .endswith("a10,10 0 1,0 -20,0 ") + ) + assert ( + page.locator("[data-feature=cap27]") + .get_attribute("d") + .endswith("a6,6 0 1,0 -12,0 ") + ) + assert ( + page.locator("[data-feature=capa8]") + .get_attribute("d") + .endswith("a4,4 0 1,0 -8,0 ") + ) + assert ( + page.locator("[data-feature=capa6]") + .get_attribute("d") + .endswith("a3,3 0 1,0 -6,0 ") + ) + assert ( + page.locator("[data-feature=capa4]") + .get_attribute("d") + .endswith("a3,3 0 1,0 -6,0 ") + ) + assert ( + page.locator("[data-feature=capa3]") + .get_attribute("d") + .endswith("a2,2 0 1,0 -4,0 ") + ) + assert ( + page.locator("[data-feature=capa2]") + .get_attribute("d") + .endswith("a2,2 0 1,0 -4,0 ") + ) + assert ( + page.locator("[data-feature=capa0]") + .get_attribute("d") + .endswith("a2,2 0 1,0 -4,0 ") + ) diff --git a/umap/tests/integration/test_edit_datalayer.py b/umap/tests/integration/test_edit_datalayer.py index b511d385..0271bac9 100644 --- a/umap/tests/integration/test_edit_datalayer.py +++ b/umap/tests/integration/test_edit_datalayer.py @@ -107,7 +107,7 @@ def test_can_change_icon_class(live_server, openmap, page): page.locator(".panel.right").get_by_title("Edit", exact=True).click() page.get_by_text("Shape properties").click() page.locator(".umap-field-iconClass a.define").click() - page.get_by_text("Circle").click() + page.get_by_text("Circle", exact=True).click() expect(page.locator(".umap-circle-icon")).to_be_visible() expect(page.locator(".umap-div-icon")).to_be_hidden()