Goal

In this tutorial, you learn how to use filtering to prevent the visualization of objects.

When you zoom out of the map, the view can clutter up with lots of overlapping icons. That clutter decreases visibility and situational awareness. You have some options to prevent icons from cluttering the view. This tutorial and the clustering tutorial discuss those options.

Starting point

We take the code written in the Drawing labels tutorial as our starting point, and expand from there.

Step 1 - Create the layer loading strategy

We want to restrict the amount of visualized data when the user sees the map from a larger distance, after zooming out for instance. The WFS that serves the vector data, supports OGC Filter parameters, so we use OGC Filters to define the restriction. By sending a query filter parameter to a WFS service, we can request filtered vector data: a subset of an otherwise much larger data set.

Before we create a filter query though, we need to discuss the concept of loading strategies. When a feature layer is added to a map, LuciadRIA triggers a query to load vector data, through the model Store set on the layer. That request adheres to a particular loading strategy. Each feature layer has an instance of LoadStrategy, which is either set explicitly or implicitly.

So far, we used a default loading strategy, but now we’re going to create a new one. We configure it to pass a specific filter object with the query request to the underlying WFS store.

You can choose between two options when you create a loading strategy object:

  • LoadSpatially - this strategy retrieves only the data within the visible extent of the map from the model.

  • LoadEverything - this strategy loads all data for the whole map.

You can use LoadSpatially with a Store instance that implements the spatialQuery() method only, which is the case for WFSFeatureStore.

Let’s explicitly set an instance of the LoadSpatially loading strategy in the city layer.

Modification in vectorData.ts
import {LoadSpatially} from "@luciad/ria/view/feature/loadingstrategy/LoadSpatially.js";
  // ...
  function createCityLoadingStrategy() {
    return new LoadSpatially();
  }

  export function createCitiesLayer(map: Map) {
    // ...
    const featureLayer = new FeatureLayer(featureModel, {
      label: "US Cities",
      layerType: LayerType.STATIC,
      painter: addSelection(new CityPainter()),
      selectable: true,
      loadingStrategy: createCityLoadingStrategy()
    });
  }

If a feature layer uses a model containing a WFSFeatureStore, the LoadSpatially loading strategy is implicitly applied.

For more information about loading strategies, see Dealing with large feature data sets.

Step 2 - Configure a loading strategy with QueryProvider

You can configure the loading strategy with a QueryProvider object that must implement these callback functions:

  • getQueryLevelScales() - Must return an array of map scale thresholds. If you don’t define thresholds, the same query object is used at all map scales.

  • getQueryForLevel(scaleLevel) - Returns a query object to use on the backing Store for the given scale level.

Let’s create a QueryProvider instance that defines two scale levels with a distinct query object for each. The getQueryLevelScales function defines two scale levels:

  • Level 0: when a map scale is below the threshold value (1/50000000), the query request isn’t executed at all. The value QueryProvider.QUERY_NONE means that there’s no query.

  • Level 1: when the map scale is above the threshold, the query request to WFS doesn’t specify any filters.

import {LoadSpatially} from "@luciad/ria/view/feature/loadingstrategy/LoadSpatially.js";
import {QueryProvider} from "@luciad/ria/view/feature/QueryProvider.js";

  class CityQueryProvider extends QueryProvider {
    getQueryLevelScales() {
      return [1 / 50000000];
    }

    getQueryForLevel(level: number) {
      return level === 0 ? QueryProvider.QUERY_NONE : null;
    }
  }

  function createCityLoadingStrategy() {
    // use dedicated
    return new LoadSpatially({queryProvider: new CityQueryProvider()});
  }

Now, when you zoom out, yo see no cities displayed. When you zoom in, they re-appear.

Step 3 - Create a query filter object

This isn’t what we wanted to achieve, so let’s continue modifying the CityQueryProvider to request a subset of city features based on an OGC Query Filter parameter. Instead of using QueryProvider.QUERY_NONE, we want to create a filter object that a WFS service can process. We can do use the utility functions from the FilterFactory for that. Our aim is to limit our request to the big cities only when we zoom out to scale level 0.

The snippet below shows you how to create a filter object for features that have a "TOT_POP" property value greater than or equal to 1000000.

import {gte, literal, property} from "@luciad/ria/ogc/filter/FilterFactory.js";
  const BIG_CITY_FILTER = gte(property("TOT_POP"), literal(1000000));

Let’s use the big city filter in the QueryProvider for a scale level of 0.

const BIG_CITY_FILTER = gte(property("TOT_POP"), literal(1000000));

class CityQueryProvider extends QueryProvider {
  getQueryLevelScales() {
    return [1 / 50000000];
  }

  getQueryForLevel(level: number) {
    return level === 0 ? {filter: BIG_CITY_FILTER} : null;
  }
}

Final result

The map now behaves in such a way that when we zoom out, we see just the big cities. After zooming in, we see all the cities.

filtering
Figure 1. Only big cities are visualized when zoomed out.

Full code

vectorData
import {FeatureLayer} from "@luciad/ria/view/feature/FeatureLayer.js";
import {FeatureModel} from "@luciad/ria/model/feature/FeatureModel.js";
import {LayerType} from "@luciad/ria/view/LayerType.js";
import {Map} from "@luciad/ria/view/Map.js";
import {WFSFeatureStore} from "@luciad/ria/model/store/WFSFeatureStore.js";
import {CityPainter} from "./CityPainter.js";
import {addSelection} from "@luciad/ria/view/feature/FeaturePainterUtil.js";
import {LoadSpatially} from "@luciad/ria/view/feature/loadingstrategy/LoadSpatially.js";
import {QueryProvider} from "@luciad/ria/view/feature/QueryProvider.js";
import {gte, literal, property} from "@luciad/ria/ogc/filter/FilterFactory.js";

const BIG_CITY_FILTER = gte(property("TOT_POP"), literal(1000000));

class CityQueryProvider extends QueryProvider {
  getQueryLevelScales() {
    return [1 / 50000000];
  }

  getQueryForLevel(level: number) {
    return level === 0 ? {filter: BIG_CITY_FILTER} : null;
  }
}

function createCityLoadingStrategy() {
  return new LoadSpatially({queryProvider: new CityQueryProvider()});
}

export function createCitiesLayer(map: Map) {
  const url = "https://sampleservices.luciad.com/wfs";
  const featureTypeName = "cities";
  // Create a WFS store
  return WFSFeatureStore.createFromURL(url, featureTypeName).then(
      (wfsSore: WFSFeatureStore) => {
        // Create a model based on the created store
        const featureModel = new FeatureModel(wfsSore);
        // Create a feature layer
        const featureLayer = new FeatureLayer(featureModel, {
          label: "US Cities",
          layerType: LayerType.STATIC,
          painter: addSelection(new CityPainter()),
          selectable: true,
          loadingStrategy: createCityLoadingStrategy()
        });

        // Add a layer to the map
        map.layerTree.addChild(featureLayer);
        return featureLayer;
      });
}