Goal

On the default LuciadRIA map, a user can navigate and zoom using the mouse. The MapNavigator class in the API also provides utility functions to navigate the map programmatically. In this tutorial, you find out what you can do with MapNavigator:

This tutorial explains the most common functions. For a complete list of functions and detailed information about these functions, see the MapNavigator API documentation.

Starting point

In this tutorial, we take the code from Learn how to load and display vector data on a LuciadRIA map tutorial as the starting point, and expand it with new MapNavigator functions.

Fitting to a bounding box

Fitting to a pre-defined area

If you want to fit to an area on the map, you can use the fit function. For example, after loading your application and adding your layers, you want to fit the map to an area that shows data of interest. In such a case, you can fit your map to that area using this code:

Fitting to a Bounds
import {getReference} from "@luciad/ria/reference/ReferenceProvider.js";
import {createBounds} from "@luciad/ria/shape/ShapeFactory.js";
const reference = getReference("CRS:84");
const bounds = createBounds(reference,
    [-158.22554883458136, 98.56866079487072, 20.627551367182754, 40.905334466412214]);
map.mapNavigator.fit({
  bounds: bounds,
  animate: true
});

The code ensures that the map fits to the USA. If you set the animate property to true, you add an animation effect to the fitting operation, and the fit function returns a Promise. You can chain animations with that Promise: the Promise is resolved when the animation is complete. At that point, you can start a new animation. If the animation is interrupted, for example when a user starts a MapNavigator operation while the animation is in progress, the Promise is rejected with a CancelError.

Even if animate is false, you still need to wait for the returned Promise to resolve if you want to be sure that the map has navigated to the target position.

Fitting to a layer

The preceding example fits the map to a pre-defined area, and uses a statically defined Bounds. If you don’t know in which area the interesting data is visible, though, you can fit on the layer bounds instead. You can use the same fit function, but with the layer bounds this time.

The function that fits to the layer bounds
// Fitting to the bounds of a raster layer
map.mapNavigator.fit({
  bounds: layer.model.bounds,
  animate: true
});

Fitting to a vector layer

LuciadRIA calculates the boundary of a layer dynamically. Whenever you add an object to the layer, or remove an object from the layer, LuciadRIA re-calculates the layer boundary. We use the QueryFinished event of the FeatureLayer 's WorkingSet to make sure that the data in the layer is loaded, and the boundary of the layer is calculated:

The function that creates the cities layer
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";

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,
          selectable: true
        });
        // Add a layer to the map
        map.layerTree.addChild(featureLayer);
        return featureLayer;
      });
}
Function that fits to the data of the layer when its data is loaded and the Bounds is ready
createCitiesLayer(map).then(function(citiesLayer) {
  const queryFinishedHandle = citiesLayer.workingSet.on("QueryFinished", function() {
    if (citiesLayer.bounds) {
      map.mapNavigator.fit({
        bounds: citiesLayer.bounds,
        animate: true
      });
    }
    queryFinishedHandle.remove();
  });
});

Panning to a selected feature

The pan function is another function of MapNavigator. You use it to change the center of the map, or shift the map in a certain direction. You can use the pan function with a point in the model, map, or view reference. With the MapNavigatorPanOptions, you can configure the pan operation. Note that the MapNavigatorPanOptions object also has an animate property to enable the animation effect.

This example code pans the map to the selected city when a user selects a city object. To detect changes in the object selection, we must first make the layer selectable:

Before we can select features, we must make the feature layer selectable
// Create a feature layer
const featureLayer = new FeatureLayer(featureModel, {
  label: "US Cities",
  layerType: LayerType.STATIC,
  selectable: true
});

We then add an event listener to the SelectionChanged event of the map. If the user selects a new city on the map, we pan the map to that city.

The main.ts file updated with the action to take when a user clicks the zoom-in or zoom-out buttons
map.on("SelectionChanged", function(selectionChangeEvent) {
  const selectedCity = selectionChangeEvent.selectionChanges[0].selected[0];
  if (selectedCity && selectedCity.shape && selectedCity.shape.focusPoint) {
    map.mapNavigator.pan({
      animate: true,
      targetLocation: selectedCity.shape.focusPoint
    });
  }
});

Zooming in or out

If you want to zoom in or zoom out on the map, or if you want to change the scale of the map, you can use the zoom function. You can use a single zoom function for both zoom-in and zoom-out operations. Just like the methods in the MapNavigator class, zoom works with a MapNavigatorZoomOptions object.

In the example, we add two new buttons to the map. The map zooms in or out, depending on the button that the user clicks. When you call the zoom functions, you need to specify the target scale or zoom factor. In this example, we use the zoom factor. The zoom factor determines whether to zoom in or out on the map. If the zoom factor value is greater than 1, LuciadRIA zooms in on the map. If it isn’t, LuciadRIA zooms the map out.

First, we need to add zoom-in and zoom-out buttons to the index.html file.

The map <div> element, updated with new <button> elements for the zoom-in and zoom-out operations
<div id="map">
  <button id="zoom-in">+</button>
  <button id="zoom-out">-</button>
</div>

Now we add some CSS to main.css to make these buttons appear on the right of the map:

The CSS updated with new rules for our new buttons
#zoom-in {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 1;
  font-size: 30px;
  padding: 5px;
  width: 30px;
}

#zoom-out {
  position: absolute;
  top: 75px;
  right: 10px;
  z-index: 1;
  font-size: 30px;
  padding: 5px;
  width: 30px;
}

Now, we can add the logic that we want to run when a user clicks the new buttons.

The main.ts file updated with the action to take when the user clicks the zoom-in or zoom-out buttons
// Add click event listeners to the zoom in and zoom out buttons
const zoomInButton = document.getElementById("zoom-in")!;
zoomInButton.addEventListener("click", function() {
  map.mapNavigator.zoom({
    factor: 1.25,
    animate: true
  });
});

const zoomOutButton = document.getElementById("zoom-out")!;
zoomOutButton.addEventListener("click", function() {
  map.mapNavigator.zoom({
    factor: 0.75,
    animate: true
  });
});

Full code

index.html
<!doctype html>
<html lang="en">
<head>
  <title>Hello world</title>
  <link href="main.css" rel="stylesheet"/>
</head>
<body>
<h1>Hello world</h1>

<div id="map">
  <button id="zoom-in">+</button>
  <button id="zoom-out">-</button>
</div>
index.ts
import {WMSTileSetModel} from "@luciad/ria/model/tileset/WMSTileSetModel.js";
import {RasterTileSetLayer} from "@luciad/ria/view/tileset/RasterTileSetLayer.js";

import {getReference} from "@luciad/ria/reference/ReferenceProvider.js";
import {createBounds} from "@luciad/ria/shape/ShapeFactory.js";
import {createCitiesLayer} from "./vectorData.js";
import {WebGLMap} from "@luciad/ria/view/WebGLMap.js";

//Create a new map instance, and display it in the div with the "map" id
const map = new WebGLMap("map");

//Add some WMS data to the map
const server = "https://sampleservices.luciad.com/wms";
const dataSetName = "4ceea49c-3e7c-4e2d-973d-c608fb2fb07e";
WMSTileSetModel.createFromURL(server, [{layer: dataSetName}])
    .then(model => {

      const layer = new RasterTileSetLayer(model);
      map.layerTree.addChild(layer, "bottom");

      if (layer.model && layer.model.bounds) {
        // Fitting to the bounds of a raster layer
        map.mapNavigator.fit({
          bounds: layer.model.bounds,
          animate: true
        });
      }
    });

createCitiesLayer(map).then(function(citiesLayer) {
  const queryFinishedHandle = citiesLayer.workingSet.on("QueryFinished", function() {
    if (citiesLayer.bounds) {
      map.mapNavigator.fit({
        bounds: citiesLayer.bounds,
        animate: true
      });
    }
    queryFinishedHandle.remove();
  });
});

map.on("SelectionChanged", function(selectionChangeEvent) {
  const selectedCity = selectionChangeEvent.selectionChanges[0].selected[0];
  //@ts-ignore
  if (selectedCity && selectedCity.shape && selectedCity.shape.focusPoint) {
    map.mapNavigator.pan({
      animate: true,
      //@ts-ignore
      targetLocation: selectedCity.shape.focusPoint
    });
  }
});

const reference = getReference("CRS:84");
const bounds = createBounds(reference,
    [-158.22554883458136, 98.56866079487072, 20.627551367182754, 40.905334466412214]);
map.mapNavigator.fit({
  bounds: bounds,
  animate: true
});

// Add click event listeners to the zoom in and zoom out buttons
const zoomInButton = document.getElementById("zoom-in")!;
zoomInButton.addEventListener("click", function() {
  map.mapNavigator.zoom({
    factor: 1.25,
    animate: true
  });
});

const zoomOutButton = document.getElementById("zoom-out")!;
zoomOutButton.addEventListener("click", function() {
  map.mapNavigator.zoom({
    factor: 0.75,
    animate: true
  });
});
main.css
#map {
  position: relative;
  width: 100%;
  height: 620px;
  overflow: hidden;
  border: 1px solid grey;
}

#zoom-in {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 1;
  font-size: 30px;
  padding: 5px;
  width: 30px;
}

#zoom-out {
  position: absolute;
  top: 75px;
  right: 10px;
  z-index: 1;
  font-size: 30px;
  padding: 5px;
  width: 30px;
}
vectorData.ts
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";

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,
          selectable: true
        });
        // Add a layer to the map
        map.layerTree.addChild(featureLayer);
        return featureLayer;
      });
}