Users of your application want to see and change the map scale in a way in a familiar way. There should be a direct relationship between a unit of measurement on the map and the actual distance. Most users are very familiar with the scale indications displayed on paper maps. Paper maps usually display the scale in the margin of the map, and use fractional scales and bar scales. Those are just about the simplest way to convey map scaling information.

papermapscales
Figure 1. Fractional scale on top of a bar scale on a paper map

This article shows you how to display the map scale in LuciadLightspeed views. It starts by explaining more about the nature of map scales. Next, it describes how to retrieve and set paper map scales in LuciadLightspeed views. Finally, it shows you how to let your application users change the map scale by zooming in and out between pre-defined scale levels.

Types of map scales

What are fractional map scales?

Fractional map scales relate a real distance on a map to a real distance on the earth, and represent this relationship as a fraction. For instance, a map scale of 1:20,000 tells us that 1 centimeter on the map represents 20,000 centimeter (200 m) in the real world, as you can see in the image below.

LuciadLightspeed supports fractional map scales. Lucy displays fractional map scales in 2D map tool bars.

mapscale

What are bar scales?

Bar scales are even easier to figure out than fractional scales. They visually represent the relationship between a distance on a map and a real distance on the earth. Using a bar scale, your users can understand a map’s scale at a single glance. The length of a bar unit represents the earth distance displayed above the bar.

What scales does LuciadLightspeed use?

Both the Lightspeed view and GXY view APIs directly support paper map scales. You can easily recognize paper map scales by the use of the class TLcdMapScale.

Some API methods, such as ILcdGXYView.getScale, use a slightly different definition of scale. They define scale as the number of pixels per world unit, as determined by the view’s world reference. Here, the scale relates screen pixels to a real distance on the earth. For example, 1 pixel on the screen could represent 20,000 meter (200 km) in the real world.

Overlay components for scale widgets

LuciadLightspeed offers specific map overlay components for scales in GXY views and Lightspeed views: TLcdGXYScaleIndicator, TLspScaleIndicator and TLspFXScaleIndicator. Their default look is a bar scale, and by clicking on it the user can switch to the fractional representation and back.

barscale
Figure 2. Bar scale representation
fractionscale
Figure 3. Fractional scale representation

Accurately scaling the map

On most maps, a distance of 1 cm measured at two different spots on the map can cover quite a different distance in the real world. To represent the Earth on a 2D map, the Earth’s surface is projected on a plane. Such a projection almost inevitably leads to distortions. The commonly used Mercator projection, for example, distorts sizes and distances. The distance represented by a scale unit converges to zero as you move towards the poles. The next image illustrates this.

distances
Figure 4. Scale changes in the Mercator projection

That’s why the scale of a 2D map is usually measured at the origin of the projection, because this is supposed to be the area with the least amount of distortion.

In 3D maps, determining the map scale is even harder, which you can easily imagine when you look towards the horizon. Perspective becomes an additional factor in map projection and scaling. Usually the map scale is measured in a focus point on the map, meaning the center of the area you’re currently looking at.

If you look at the image with the two ruler measurements, you’ll notice that bar scales display the scale at the origin of the projection. This is a default setting, which you can still change. To configure the scale origin, use the setScaleAtCenterOfMap() method. It displays the scale at the center of the part of the world you’re currently looking at.

If you use this setting, the bar scale will constantly change when a user pans around the map.

Retrieve and set paper map scales

Here’s how you retrieve the paper map scale of a Lightspeed map:

Program: Retrieving the paper map scale of a Lightspeed map
// refer to the ScaleLocation javadoc for more information
TLcdMapScale globalMapScale = view.getViewXYZWorldTransformation().getMapScale(ScaleLocation.PROJECTION_CENTER);
System.out.println("The map scale at the center of the projection is " + globalMapScale);
TLcdMapScale localMapScale = view.getViewXYZWorldTransformation().getMapScale(ScaleLocation.MAP_CENTER);
System.out.println("The map scale at the current view extents is " + localMapScale);

Program: Setting the scale of a Lightspeed map shows you how to set a Lightspeed map to a scale of 1:50.000.

Program: Setting the scale of a Lightspeed map
new TLspViewNavigationUtil(view).zoom(new TLcdMapScale(1d / 50_000d), ScaleLocation.PROJECTION_CENTER);

To make paper map scales work correctly, LuciadLightspeed needs to know the size of a single pixel on your screen, based on its DPI pixel density. If you notice that your map scale indicator is systematically off, you might want to double-check this value. For more information, see Views and screen DPI.

Define scale levels for zooming

Using our knowledge of paper map scales and LuciadLightspeed view scales, we can come up with a widget with pre-defined zoom levels, as seen in many mapping applications.

To define our scales, we use 20 paper map scales that go all the way down to street level (1:100 scale):

Program: Defining paper map scales to street level
// paper map scales from low to high detail
private static final double[] SCALES = new double[20];

static {
  double endScale = 1.0 / 100; // we want to zoom in until street level
  double delta = 0.45;
  // derive the scales starting from the end scale
  for (int i = SCALES.length - 1; i >= 0; i--) {
    SCALES[i] = i == SCALES.length - 1 ? endScale : SCALES[i + 1] * delta;
  }
}

This code snippet shows how to define an action that zooms in to the next pre-defined level:

Program: Defining a zoom action to the next scale level
TLspViewNavigationUtil navigationUtil = new TLspViewNavigationUtil(getView());

ILcdAction zoomToNextLevel = new ALcdAction() {
  @Override
  public void actionPerformed(ActionEvent e) {
    TLcdMapScale currentScale = getView().getViewXYZWorldTransformation().getMapScale(PROJECTION_CENTER);
    double snap = ScaleSupport.snapScale(currentScale.getValue(), ScaleSupport.ZoomOperation.ZOOM_IN, SCALES);
    if (snap > 0) {
      navigationUtil.zoom(new TLcdMapScale(snap), PROJECTION_CENTER);
    }
  }
};

This example shows how to link such actions to on-map buttons:

package samples.lightspeed.controllers.fixedScales;

import static com.luciad.view.TLcdMapScale.ScaleLocation.PROJECTION_CENTER;

import java.awt.event.ActionEvent;

import javax.swing.JButton;

import com.luciad.gui.ALcdAction;
import com.luciad.gui.ILcdAction;
import com.luciad.gui.swing.TLcdSWAction;
import com.luciad.view.TLcdMapScale;
import com.luciad.view.lightspeed.util.TLspViewNavigationUtil;

import samples.common.ScaleSupport;
import samples.lightspeed.common.LightspeedSample;

/**
 * Shows how to zoom to a predefined set of scales.
 */
public class MainPanel extends LightspeedSample {

  // paper map scales from low to high detail
  private static final double[] SCALES = new double[20];

  static {
    double endScale = 1.0 / 100; // we want to zoom in until street level
    double delta = 0.45;
    // derive the scales starting from the end scale
    for (int i = SCALES.length - 1; i >= 0; i--) {
      SCALES[i] = i == SCALES.length - 1 ? endScale : SCALES[i + 1] * delta;
    }
  }

  @Override
  protected void createGUI() {
    super.createGUI();
    TLspViewNavigationUtil navigationUtil = new TLspViewNavigationUtil(getView());

    ILcdAction zoomToNextLevel = new ALcdAction() {
      @Override
      public void actionPerformed(ActionEvent e) {
        TLcdMapScale currentScale = getView().getViewXYZWorldTransformation().getMapScale(PROJECTION_CENTER);
        double snap = ScaleSupport.snapScale(currentScale.getValue(), ScaleSupport.ZoomOperation.ZOOM_IN, SCALES);
        if (snap > 0) {
          navigationUtil.zoom(new TLcdMapScale(snap), PROJECTION_CENTER);
        }
      }
    };
    zoomToNextLevel.putValue(ILcdAction.NAME, "+");

    ILcdAction zoomToPreviousLevel = new ALcdAction() {
      @Override
      public void actionPerformed(ActionEvent e) {
        TLcdMapScale currentScale = getView().getViewXYZWorldTransformation().getMapScale(PROJECTION_CENTER);
        double snap = ScaleSupport.snapScale(currentScale.getValue(), ScaleSupport.ZoomOperation.ZOOM_OUT, SCALES);
        if (snap > 0) {
          navigationUtil.zoom(new TLcdMapScale(snap), PROJECTION_CENTER);
        }
      }
    };
    zoomToPreviousLevel.putValue(ILcdAction.NAME, "-");

    getOverlayPanel().add(new JButton(new TLcdSWAction(zoomToNextLevel)));
    getOverlayPanel().add(new JButton(new TLcdSWAction(zoomToPreviousLevel)));

  }

  public static void main(String[] args) {
    startSample(MainPanel.class, "Zooming using fixed scales");
  }
}