sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From jso...@apache.org
Subject [sis] branch geoapi-4.0 updated: Portrayal : add portrayal rule and query tests, implements events on MapLayers components
Date Mon, 18 Jan 2021 14:03:10 GMT
This is an automated email from the ASF dual-hosted git repository.

jsorel pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new fb27a92  Portrayal : add portrayal rule and query tests, implements events on MapLayers
components
fb27a92 is described below

commit fb27a9201e97441515d1e6ad4f8bbec814cc52de
Author: jsorel <johann.sorel@geomatys.com>
AuthorDate: Mon Jan 18 14:59:39 2021 +0100

    Portrayal : add portrayal rule and query tests, implements events on MapLayers components
---
 .../apache/sis/internal/map/ListChangeEvent.java   |  98 +++++++++
 .../org/apache/sis/internal/map/NotifiedList.java  |  72 +++++++
 .../org/apache/sis/internal/map/SEPortrayer.java   |   4 +-
 .../java/org/apache/sis/portrayal/MapLayers.java   |  44 +++-
 .../java/org/apache/sis/portrayal/Observable.java  |  30 ++-
 .../java/org/apache/sis/internal/map/MockRule.java |   2 +-
 .../apache/sis/internal/map/SEPortrayerTest.java   | 234 ++++++++++++++++++++-
 .../org/apache/sis/portrayal/MapLayersTest.java    |  99 +++++++++
 .../apache/sis/test/suite/PortrayalTestSuite.java  |   1 +
 9 files changed, 570 insertions(+), 14 deletions(-)

diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/ListChangeEvent.java
b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/ListChangeEvent.java
new file mode 100644
index 0000000..311c830
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/ListChangeEvent.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.map;
+
+import java.beans.PropertyChangeEvent;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.apache.sis.measure.NumberRange;
+
+/**
+ * Event generated by modified list properties.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class ListChangeEvent<T> extends PropertyChangeEvent {
+
+    public enum Type {
+        ADDED,
+        REMOVED,
+        CHANGED
+    }
+
+    private final NumberRange<Integer> range;
+    private final Type type;
+    private final List<T> items;
+
+    private ListChangeEvent(final Object source, String propertyName, List<T> originalList,
final List<? extends T> items, final NumberRange<Integer> range, final Type type){
+        super(source, propertyName, originalList, originalList);
+        this.range = range;
+        this.type = type;
+        this.items = (items != null) ? Collections.unmodifiableList(new ArrayList<>(items))
: null;
+    }
+
+    /**
+     * Returns the range index of the affected items.
+     * If the event type is Added, the range correspond to the index range after insertion.
+     * If the event type is Removed, the range correspond to the index before deletion.
+     * @return NumberRange added or removed range.
+     */
+    public NumberRange<Integer> getRange(){
+        return range;
+    }
+
+    /**
+     * Returns event type.
+     */
+    public Type getType(){
+        return type;
+    }
+
+    /**
+     * Returns the affected items of this event.
+     * This property is set if event is of type added or removed.
+     *
+     * @return List
+     */
+    public List<T> getItems(){
+        return items;
+    }
+
+    public static <T> ListChangeEvent<T> added(Object source, String propertyName,
List<T> originalList, T newItem, final int index) {
+        return added(source, propertyName, originalList, Arrays.asList(newItem),
+                NumberRange.create(index, true, index, true));
+    }
+
+    public static <T> ListChangeEvent<T> added(Object source, String propertyName,
List<T> originalList, List<T> newItems, final NumberRange<Integer> range)
{
+        return new ListChangeEvent<>(source, propertyName, originalList,  newItems,
range, Type.ADDED);
+    }
+
+    public static <T> ListChangeEvent<T> removed(Object source, String propertyName,
List<T> originalList, T newItem, final int index) {
+        return removed(source, propertyName, originalList, Arrays.asList(newItem),
+                NumberRange.create(index, true, index, true));
+    }
+
+    public static <T> ListChangeEvent<T> removed(Object source, String propertyName,
List<T> originalList, List<T> oldItems, final NumberRange<Integer> range)
{
+        return new ListChangeEvent<>(source, propertyName, originalList, oldItems,
range, Type.REMOVED);
+    }
+
+    public static <T> ListChangeEvent<T> changed(Object source, String propertyName,
List<T> originalList) {
+        return new ListChangeEvent<>(source, propertyName, originalList, null, null,
Type.CHANGED);
+    }
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/NotifiedList.java
b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/NotifiedList.java
new file mode 100644
index 0000000..20fe558
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/NotifiedList.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.map;
+
+import java.util.AbstractList;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import org.apache.sis.measure.NumberRange;
+
+/**
+ * Decorate a CopyOnWriteArrayList and notify changes when elements are added or removed.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public abstract class NotifiedList<T> extends AbstractList<T> {
+
+    private final CopyOnWriteArrayList<T> parent = new CopyOnWriteArrayList<>();
+
+    @Override
+    public T get(int index) {
+        return parent.get(index);
+    }
+
+    @Override
+    public int size() {
+        return parent.size();
+    }
+
+    @Override
+    public T set(int index, T element) {
+        final T old = parent.set(index, element);
+        notifyReplace(old, element, index);
+        return old;
+    }
+
+    @Override
+    public void add(int index, T element) {
+        parent.add(index, element);
+        notifyAdd(element, index);
+    }
+
+    @Override
+    public T remove(int index) {
+        final T old = parent.remove(index);
+        notifyRemove(old, index);
+        return old;
+    }
+
+    protected abstract void notifyAdd(final T item, int index);
+
+    protected abstract void notifyAdd(final List<T> items, NumberRange<Integer>
range);
+
+    protected abstract void notifyRemove(final T item, int index);
+
+    protected abstract void notifyRemove(final List<T> items, NumberRange<Integer>
range);
+
+    protected abstract void notifyReplace(final T olditem, final T newitem, int index);
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/SEPortrayer.java
b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/SEPortrayer.java
index 29c951f..19283c0 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/SEPortrayer.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/SEPortrayer.java
@@ -98,8 +98,8 @@ import org.opengis.util.GenericName;
  * </ul>
  *
  * @author  Johann Sorel (Geomatys)
- * @version 2.0
- * @since   2.0
+ * @version 1.1
+ * @since   1.1
  * @module
  */
 public final class SEPortrayer {
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/MapLayers.java b/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/MapLayers.java
index e5f8429..d58303c 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/MapLayers.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/MapLayers.java
@@ -16,13 +16,15 @@
  */
 package org.apache.sis.portrayal;
 
-import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
-import org.opengis.geometry.Envelope;
-import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.apache.sis.geometry.ImmutableEnvelope;
+import org.apache.sis.internal.map.ListChangeEvent;
+import org.apache.sis.internal.map.NotifiedList;
+import org.apache.sis.measure.NumberRange;
 import org.apache.sis.storage.DataSet;
+import org.opengis.geometry.Envelope;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
 
 
 /**
@@ -55,11 +57,44 @@ public class MapLayers extends MapItem {
     public static final String AREA_OF_INTEREST_PROPERTY = "areaOfInterest";
 
     /**
+     * The {@value} property name, used for notifications about changes in map item components.
+     *
+     * @see #getComponents()
+     * @see #addPropertyChangeListener(String, PropertyChangeListener)
+     */
+    public static final String COMPONENTS_PROPERTY = "components";
+
+    /**
      * The components in this group, or an empty list if none.
      *
      * @todo Should be an observable list with event sent when an element is added/removed/modified.
      */
-    private final List<MapItem> components;
+    private final List<MapItem> components = new NotifiedList<MapItem>() {
+        @Override
+        protected void notifyAdd(MapItem item, int index) {
+            firePropertyChange(ListChangeEvent.added(MapLayers.this, COMPONENTS_PROPERTY,
components, item, index));
+        }
+
+        @Override
+        protected void notifyAdd(List<MapItem> items, NumberRange<Integer> range)
{
+            firePropertyChange(ListChangeEvent.added(MapLayers.this, COMPONENTS_PROPERTY,
components, items, range));
+        }
+
+        @Override
+        protected void notifyRemove(MapItem item, int index) {
+            firePropertyChange(ListChangeEvent.removed(MapLayers.this, COMPONENTS_PROPERTY,
components, item, index));
+        }
+
+        @Override
+        protected void notifyRemove(List<MapItem> items, NumberRange<Integer>
range) {
+            firePropertyChange(ListChangeEvent.removed(MapLayers.this, COMPONENTS_PROPERTY,
components, items, range));
+        }
+
+        @Override
+        protected void notifyReplace(MapItem olditem, MapItem newitem, int index) {
+            firePropertyChange(ListChangeEvent.changed(MapLayers.this, COMPONENTS_PROPERTY,
components));
+        }
+    };
 
     /**
      * The area of interest, or {@code null} is unspecified.
@@ -70,7 +105,6 @@ public class MapLayers extends MapItem {
      * Creates an initially empty group of layers.
      */
     public MapLayers() {
-        components = new ArrayList<>();
     }
 
     /**
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/Observable.java b/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/Observable.java
index 67ca9c7..e68e4af 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/Observable.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/Observable.java
@@ -16,12 +16,12 @@
  */
 package org.apache.sis.portrayal;
 
-import java.util.Map;
-import java.util.HashMap;
-import java.util.Arrays;
-import java.util.ConcurrentModificationException;
 import java.beans.PropertyChangeEvent;
 import java.beans.PropertyChangeListener;
+import java.util.Arrays;
+import java.util.ConcurrentModificationException;
+import java.util.HashMap;
+import java.util.Map;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.ArraysExt;
 
@@ -150,6 +150,28 @@ abstract class Observable {
     }
 
     /**
+     * Notifies all registered listeners that a property of the given name changed its value.
+     * It is caller responsibility to verify that the event source and property name are
valid.
+     *
+     * @param  event the event to forward, can not be null.
+     *
+     * @see PropertyChangeEvent
+     * @see PropertyChangeListener
+     */
+    protected void firePropertyChange(final PropertyChangeEvent event) {
+        ArgumentChecks.ensureNonNull("event", event);
+
+        if (listeners != null) {
+            final PropertyChangeListener[] list = listeners.get(event.getPropertyName());
+            if (list != null) {
+                for (final PropertyChangeListener listener : list) {
+                    listener.propertyChange(event);
+                }
+            }
+        }
+    }
+
+    /**
      * Returns {@code true} if the given property has at least one listener.
      *
      * @param  propertyName  name of the property.
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/map/MockRule.java b/core/sis-portrayal/src/test/java/org/apache/sis/internal/map/MockRule.java
index d8f38c0..f3eecee 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/internal/map/MockRule.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/internal/map/MockRule.java
@@ -65,7 +65,7 @@ public final class MockRule implements Rule {
         return legend;
     }
 
-    public void setLegeng(GraphicLegend legend) {
+    public void setLegend(GraphicLegend legend) {
         this.legend = legend;
     }
 
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/map/SEPortrayerTest.java
b/core/sis-portrayal/src/test/java/org/apache/sis/internal/map/SEPortrayerTest.java
index 02ae357..b7fdb2e 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/internal/map/SEPortrayerTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/internal/map/SEPortrayerTest.java
@@ -30,8 +30,12 @@ import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.coverage.grid.GridOrientation;
 import org.apache.sis.feature.builder.AttributeRole;
 import org.apache.sis.feature.builder.FeatureTypeBuilder;
+import org.apache.sis.filter.DefaultFilterFactory;
+import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.internal.feature.AttributeConvention;
 import org.apache.sis.internal.storage.MemoryFeatureSet;
+import org.apache.sis.internal.storage.query.SimpleQuery;
+import org.apache.sis.internal.system.DefaultFactories;
 import org.apache.sis.portrayal.MapItem;
 import org.apache.sis.portrayal.MapLayer;
 import org.apache.sis.portrayal.MapLayers;
@@ -48,6 +52,11 @@ import org.locationtech.jts.geom.Point;
 import org.locationtech.jts.geom.Polygon;
 import org.opengis.feature.Feature;
 import org.opengis.feature.FeatureType;
+import org.opengis.filter.Filter;
+import org.opengis.filter.FilterFactory;
+import org.opengis.filter.FilterFactory2;
+import org.opengis.filter.identity.Identifier;
+import org.opengis.geometry.Envelope;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.style.SemanticType;
 import org.opengis.style.Symbolizer;
@@ -58,11 +67,18 @@ import org.opengis.style.Symbolizer;
  */
 public class SEPortrayerTest extends TestCase {
 
+    private final FilterFactory2 filterFactory;
     private final FeatureSet fishes;
     private final FeatureSet boats;
 
     public SEPortrayerTest() {
 
+        FilterFactory filterFactory = DefaultFactories.forClass(FilterFactory.class);
+        if (!(filterFactory instanceof FilterFactory2)) {
+            filterFactory = new DefaultFilterFactory();
+        }
+        this.filterFactory = (FilterFactory2) filterFactory;
+
         final GeometryFactory gf = new GeometryFactory();
         final CoordinateReferenceSystem crs = CommonCRS.WGS84.normalizedGeographic();
 
@@ -78,7 +94,7 @@ public class SEPortrayerTest extends TestCase {
         fish1.setPropertyValue("id", "1");
         fish1.setPropertyValue("geom", point1);
 
-        final Point point2 = gf.createPoint(new Coordinate(1, 1));
+        final Point point2 = gf.createPoint(new Coordinate(10, 20));
         point2.setUserData(crs);
         final Feature fish2 = fishType.newInstance();
         fish2.setPropertyValue("id", "2");
@@ -109,7 +125,11 @@ public class SEPortrayerTest extends TestCase {
     }
 
     private Set<Entry<String, Symbolizer>> present(MapItem item) {
-        final GridGeometry grid = new GridGeometry(new GridExtent(360, 180), CRS.getDomainOfValidity(CommonCRS.WGS84.normalizedGeographic()),
GridOrientation.REFLECTION_Y);
+        return present(item, CRS.getDomainOfValidity(CommonCRS.WGS84.normalizedGeographic()));
+    }
+
+    private Set<Entry<String, Symbolizer>> present(MapItem item, Envelope env)
{
+        final GridGeometry grid = new GridGeometry(new GridExtent(360, 180), env, GridOrientation.REFLECTION_Y);
         final SEPortrayer portrayer = new SEPortrayer();
         final Stream<Presentation> stream = portrayer.present(grid, item);
         final List<Presentation> presentations = stream.collect(Collectors.toList());
@@ -164,6 +184,81 @@ public class SEPortrayerTest extends TestCase {
     }
 
     /**
+     * Test portrayer includes the bounding box of the canvas while querying features.
+     * Only fish feature with identifier "2" matches in this test.
+     */
+    @Test
+    public void testCanvasBboxfilter() {
+
+        final GeneralEnvelope env = new GeneralEnvelope(CommonCRS.WGS84.normalizedGeographic());
+        env.setRange(0, 9, 11);
+        env.setRange(1, 19, 21);
+
+        final MockStyle style = new MockStyle();
+        final MockFeatureTypeStyle fts = new MockFeatureTypeStyle();
+        final MockRule rule = new MockRule();
+        final MockLineSymbolizer symbolizer = new MockLineSymbolizer();
+        style.featureTypeStyles().add(fts);
+        fts.rules().add(rule);
+        rule.symbolizers().add(symbolizer);
+
+
+        final MapLayer fishLayer = new MapLayer();
+        fishLayer.setData(fishes);
+        fishLayer.setStyle(style);
+        final MapLayer boatLayer = new MapLayer();
+        boatLayer.setData(boats);
+        boatLayer.setStyle(style);
+        final MapLayers layers = new MapLayers();
+        layers.getComponents().add(fishLayer);
+        layers.getComponents().add(boatLayer);
+
+        final Set<Entry<String, Symbolizer>> presentations = present(layers,
env);
+        assertEquals(1, presentations.size());
+        assertTrue(presentations.contains(new AbstractMap.SimpleEntry<>("2", symbolizer)));
+    }
+
+    /**
+     * Test portrayer uses the user defined query when portraying.
+     * Only fish feature with identifier "1" and boat feature with identifier "20" matches
in this test.
+     */
+    @Test
+    public void testUserQuery() {
+
+        final MockStyle style = new MockStyle();
+        final MockFeatureTypeStyle fts = new MockFeatureTypeStyle();
+        final MockRule rule = new MockRule();
+        final MockLineSymbolizer symbolizer = new MockLineSymbolizer();
+        style.featureTypeStyles().add(fts);
+        fts.rules().add(rule);
+        rule.symbolizers().add(symbolizer);
+
+        final Set<Identifier> ids = new HashSet<>();
+        ids.add(filterFactory.featureId("1"));
+        ids.add(filterFactory.featureId("20"));
+        final Filter filter = filterFactory.id(ids);
+        final SimpleQuery query = new SimpleQuery();
+        query.setFilter(filter);
+
+        final MapLayer fishLayer = new MapLayer();
+        fishLayer.setData(fishes);
+        fishLayer.setStyle(style);
+        fishLayer.setQuery(query);
+        final MapLayer boatLayer = new MapLayer();
+        boatLayer.setData(boats);
+        boatLayer.setStyle(style);
+        boatLayer.setQuery(query);
+        final MapLayers layers = new MapLayers();
+        layers.getComponents().add(fishLayer);
+        layers.getComponents().add(boatLayer);
+
+        final Set<Entry<String, Symbolizer>> presentations = present(layers);
+        assertEquals(2, presentations.size());
+        assertTrue(presentations.contains(new AbstractMap.SimpleEntry<>("1", symbolizer)));
+        assertTrue(presentations.contains(new AbstractMap.SimpleEntry<>("20", symbolizer)));
+    }
+
+    /**
      * Portray using defined type names.
      * Test expect only boat type features to be rendered.
      */
@@ -228,4 +323,139 @@ public class SEPortrayerTest extends TestCase {
         assertTrue(presentations.contains(new AbstractMap.SimpleEntry<>("1", symbolizer)));
         assertTrue(presentations.contains(new AbstractMap.SimpleEntry<>("2", symbolizer)));
     }
+
+    /**
+     * Portray using defined rule filter
+     * Test expect only features with identifier equals "2" to match.
+     */
+    @Test
+    public void testRuleFilter() {
+
+        final Set<Identifier> ids = new HashSet<>();
+        ids.add(filterFactory.featureId("2"));
+        final Filter filter = filterFactory.id(ids);
+
+        final MockStyle style = new MockStyle();
+        final MockFeatureTypeStyle fts = new MockFeatureTypeStyle();
+        final MockRule rule = new MockRule();
+        rule.setFilter(filter);
+        final MockLineSymbolizer symbolizer = new MockLineSymbolizer();
+        style.featureTypeStyles().add(fts);
+        fts.rules().add(rule);
+        rule.symbolizers().add(symbolizer);
+
+
+        final MapLayer fishLayer = new MapLayer();
+        fishLayer.setData(fishes);
+        fishLayer.setStyle(style);
+        final MapLayer boatLayer = new MapLayer();
+        boatLayer.setData(boats);
+        boatLayer.setStyle(style);
+        final MapLayers layers = new MapLayers();
+        layers.getComponents().add(fishLayer);
+        layers.getComponents().add(boatLayer);
+
+        final Set<Entry<String, Symbolizer>> presentations = present(layers);
+        assertEquals(1, presentations.size());
+        assertTrue(presentations.contains(new AbstractMap.SimpleEntry<>("2", symbolizer)));
+    }
+
+    /**
+     * Portray using defined rule scale filter.
+     * Test expect only matching scale rule symbolizer to be portrayed.
+     */
+    @Test
+    public void testRuleScale() {
+
+        final MockLineSymbolizer symbolizerAbove = new MockLineSymbolizer();
+        final MockLineSymbolizer symbolizerUnder = new MockLineSymbolizer();
+        final MockLineSymbolizer symbolizerMatch = new MockLineSymbolizer();
+
+        //Symbology rendering scale here is 3.944391406060875E8
+        final MockRule ruleAbove = new MockRule();
+        ruleAbove.symbolizers().add(symbolizerAbove);
+        ruleAbove.setMinScaleDenominator(4e8);
+        ruleAbove.setMaxScaleDenominator(Double.MAX_VALUE);
+        final MockRule ruleUnder = new MockRule();
+        ruleUnder.symbolizers().add(symbolizerUnder);
+        ruleUnder.setMinScaleDenominator(0.0);
+        ruleUnder.setMaxScaleDenominator(3e8);
+        final MockRule ruleMatch = new MockRule();
+        ruleMatch.symbolizers().add(symbolizerMatch);
+        ruleMatch.setMinScaleDenominator(3e8);
+        ruleMatch.setMaxScaleDenominator(4e8);
+
+        final MockStyle style = new MockStyle();
+        final MockFeatureTypeStyle fts = new MockFeatureTypeStyle();
+        style.featureTypeStyles().add(fts);
+        fts.rules().add(ruleAbove);
+        fts.rules().add(ruleUnder);
+        fts.rules().add(ruleMatch);
+
+
+        final MapLayer fishLayer = new MapLayer();
+        fishLayer.setData(fishes);
+        fishLayer.setStyle(style);
+        final MapLayer boatLayer = new MapLayer();
+        boatLayer.setData(boats);
+        boatLayer.setStyle(style);
+        final MapLayers layers = new MapLayers();
+        layers.getComponents().add(fishLayer);
+        layers.getComponents().add(boatLayer);
+
+        final Set<Entry<String, Symbolizer>> presentations = present(layers);
+        assertEquals(4, presentations.size());
+        assertTrue(presentations.contains(new AbstractMap.SimpleEntry<>("1", symbolizerMatch)));
+        assertTrue(presentations.contains(new AbstractMap.SimpleEntry<>("2", symbolizerMatch)));
+        assertTrue(presentations.contains(new AbstractMap.SimpleEntry<>("10", symbolizerMatch)));
+        assertTrue(presentations.contains(new AbstractMap.SimpleEntry<>("20", symbolizerMatch)));
+    }
+
+    /**
+     * Portray using defined rule 'is else' property.
+     * Test expect only feature with identifier "10" to be rendered with the base rule
+     * and other features to rendered with the fallback rule.
+     */
+    @Test
+    public void testRuleElseCondition() {
+
+        final Set<Identifier> ids = new HashSet<>();
+        ids.add(filterFactory.featureId("10"));
+        final Filter filter = filterFactory.id(ids);
+
+        final MockLineSymbolizer symbolizerBase = new MockLineSymbolizer();
+        final MockLineSymbolizer symbolizerElse = new MockLineSymbolizer();
+
+        final MockRule ruleBase = new MockRule();
+        ruleBase.symbolizers().add(symbolizerBase);
+        ruleBase.setFilter(filter);
+        final MockRule ruleOther = new MockRule();
+        ruleOther.setIsElseFilter(true);
+        ruleOther.symbolizers().add(symbolizerElse);
+
+        final MockStyle style = new MockStyle();
+        final MockFeatureTypeStyle fts = new MockFeatureTypeStyle();
+        style.featureTypeStyles().add(fts);
+        fts.rules().add(ruleBase);
+        fts.rules().add(ruleOther);
+
+        final MapLayer fishLayer = new MapLayer();
+        fishLayer.setData(fishes);
+        fishLayer.setStyle(style);
+        final MapLayer boatLayer = new MapLayer();
+        boatLayer.setData(boats);
+        boatLayer.setStyle(style);
+        final MapLayers layers = new MapLayers();
+        layers.getComponents().add(fishLayer);
+        layers.getComponents().add(boatLayer);
+
+        final Set<Entry<String, Symbolizer>> presentations = present(layers);
+        assertEquals(4, presentations.size());
+        assertTrue(presentations.contains(new AbstractMap.SimpleEntry<>("1", symbolizerElse)));
+        assertTrue(presentations.contains(new AbstractMap.SimpleEntry<>("2", symbolizerElse)));
+        assertTrue(presentations.contains(new AbstractMap.SimpleEntry<>("10", symbolizerBase)));
+        assertTrue(presentations.contains(new AbstractMap.SimpleEntry<>("20", symbolizerElse)));
+    }
+
+
 }
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/portrayal/MapLayersTest.java
b/core/sis-portrayal/src/test/java/org/apache/sis/portrayal/MapLayersTest.java
new file mode 100644
index 0000000..3276f1f
--- /dev/null
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/portrayal/MapLayersTest.java
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.portrayal;
+
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.sis.internal.map.ListChangeEvent;
+import org.apache.sis.measure.NumberRange;
+import org.apache.sis.test.TestCase;
+import static org.junit.Assert.*;
+import org.junit.Test;
+
+/**
+ *
+ * @author Johann Sorel (Geomatys)
+ * @version 1.1
+ * @since 1.1
+ */
+public class MapLayersTest extends TestCase {
+
+    /**
+     * Test the maplayers components list events.
+     */
+    @Test
+    public void testListEvents() {
+
+        final MapLayers layers = new MapLayers();
+        final MapLayer layer1 = new MapLayer();
+        final MapLayer layer2 = new MapLayer();
+
+        final AtomicInteger eventNum = new AtomicInteger();
+        layers.addPropertyChangeListener(MapLayers.COMPONENTS_PROPERTY, new PropertyChangeListener()
{
+            @Override
+            public void propertyChange(PropertyChangeEvent evt) {
+                assertTrue(evt instanceof ListChangeEvent);
+                final ListChangeEvent levt = (ListChangeEvent) evt;
+                assertEquals(layers.getComponents(), levt.getOldValue());
+                assertEquals(layers.getComponents(), levt.getNewValue());
+                assertEquals(MapLayers.COMPONENTS_PROPERTY, levt.getPropertyName());
+                assertEquals(layers, levt.getSource());
+                int eventId = eventNum.incrementAndGet();
+                switch (eventId) {
+                    case 1 :
+                        assertEquals(ListChangeEvent.Type.ADDED, levt.getType());
+                        assertEquals(NumberRange.create(0, true, 0, true), levt.getRange());
+                        assertEquals(1, levt.getItems().size());
+                        assertEquals(layer1, levt.getItems().get(0));
+                        break;
+                    case 2 :
+                        assertEquals(ListChangeEvent.Type.ADDED, levt.getType());
+                        assertEquals(NumberRange.create(1, true, 1, true), levt.getRange());
+                        assertEquals(1, levt.getItems().size());
+                        assertEquals(layer2, levt.getItems().get(0));
+                        break;
+                    case 3 :
+                        assertEquals(ListChangeEvent.Type.REMOVED, levt.getType());
+                        assertEquals(NumberRange.create(1, true, 1, true), levt.getRange());
+                        assertEquals(1, levt.getItems().size());
+                        assertEquals(layer2, levt.getItems().get(0));
+                        break;
+                    case 4 :
+                        assertEquals(ListChangeEvent.Type.CHANGED, levt.getType());
+                        assertEquals(null, levt.getRange());
+                        assertEquals(null, levt.getItems());
+                        break;
+                }
+
+            }
+        });
+
+        layers.getComponents().add(layer1);
+        assertEquals(1, eventNum.get());
+
+        layers.getComponents().add(layer2);
+        assertEquals(2, eventNum.get());
+
+        layers.getComponents().remove(layer2);
+        assertEquals(3, eventNum.get());
+
+        layers.getComponents().set(0, layer2);
+        assertEquals(4, eventNum.get());
+    }
+
+}
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/test/suite/PortrayalTestSuite.java
b/core/sis-portrayal/src/test/java/org/apache/sis/test/suite/PortrayalTestSuite.java
index 0538531..e22488f 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/test/suite/PortrayalTestSuite.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/test/suite/PortrayalTestSuite.java
@@ -30,6 +30,7 @@ import org.junit.runners.Suite;
  * @module
  */
 @Suite.SuiteClasses({
+    org.apache.sis.portrayal.MapLayersTest.class,
     org.apache.sis.internal.map.SEPortrayerTest.class,
 })
 public final strictfp class PortrayalTestSuite extends TestSuite {


Mime
View raw message