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 the relationship between paper map scales and LuciadLightspeed scales, and tells you how to convert from paper map scales to LuciadLightspeed scales. Finally, it shows you how to let your application users change the map scale by zooming in and out between pre-defined scale levels.

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.

LuciadLightspeed overlay components

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
fractionscale

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 ubiquitous 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 2. 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.

What are LuciadLightspeed view scales?

Both the Lightspeed view and GXY view APIs don’t directly use paper map scales. Instead, they use a slightly more technical definition of scale. They define scale as the number of pixels per world unit, as determined by the view’s world reference. Hence, in LuciadLightspeed the scale relates screen pixels to a real distance on the earth.

For instance, a view scale of 1:20,000 tells us that 1 pixel on the screen represents 20,000 meter (200 km) in the real world.

Retrieve and set paper map scales

To go from a pixels/world unit scale to a meter/meter scale, you need to know two things:

  • The size of a single pixel on your screen. Java is usually pretty good at guessing this. To determine pixel size, see Toolkit.getDefaultToolkit().getScreenResolution() for Swing, and Screen#getDpi() for JavaFX.

    If you notice that your scale is systematically off by 10 to 20%, you might want to double-check this value.

  • The size of a single world unit. This is usually exposed by your world reference. See ILcdGridReference.getUnitOfMeasure() to retrieve the world unit size.

The sample classes LspScaleSupport and GXYScaleSupport contain the necessary functions to convert to and from a paper map scale.

For example, here’s how you retrieve the paper map scale of a Lightspeed map:

Program: Retrieving the paper map scale of a Lightspeed map
double scale = new LspScaleSupport(view).getMapProjectionOriginScale(96); // we're assuming a screen resolution of 96 DPI
// a scale of 1 : 50,000 results in 0.00002
double scaleDenominator = 1.0/scale;
System.out.println("The map scale is 1:" + scaleDenominator);

Below you can see how to set a Lightspeed map to a scale of 1:50.000.

Program: Setting the scale of a Lightspeed map
new LspScaleSupport(view).setMapProjectionOriginScale(1.0/50000.0, 96); // we're assuming a screen resolution of 96 DPI

Define scale levels for zooming

Using our knowledge of paper map scales and LuciadLightspeed view scales, it’s easy to come up with a widget with predefined 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
static double[] SCALES = new double[20];

{
  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;
  }
}

The following 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
LspScaleSupport scaleSupport = new LspScaleSupport(getView());

ILcdAction zoomToNextLevel = new ALcdAction() {
  @Override
  public void actionPerformed(ActionEvent e) {
    double currentScale = scaleSupport.getMapProjectionOriginScale(-1);
    double snap = AScaleSupport.retrieveSnappedScale(currentScale, AScaleSupport.ZoomOperation.ZOOM_IN, SCALES);
    if (snap > 0) {
      scaleSupport.setMapProjectionOriginScale(snap, -1);
    }
  }
};

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

package samples.lightspeed.controllers.fixedScales;

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.internal.samples.common.AScaleSupport;
import com.luciad.internal.samples.common.lightspeed.LspScaleSupport;

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
  static double[] SCALES = new double[20];

  {
    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();
    LspScaleSupport scaleSupport = new LspScaleSupport(getView());

    ILcdAction zoomToNextLevel = new ALcdAction() {
      @Override
      public void actionPerformed(ActionEvent e) {
        double currentScale = scaleSupport.getMapProjectionOriginScale(-1);
        double snap = AScaleSupport.retrieveSnappedScale(currentScale, AScaleSupport.ZoomOperation.ZOOM_IN, SCALES);
        if (snap > 0) {
          scaleSupport.setMapProjectionOriginScale(snap, -1);
        }
      }
    };
    zoomToNextLevel.putValue(ILcdAction.NAME, "+");

    ILcdAction zoomToPreviousLevel = new ALcdAction() {
      @Override
      public void actionPerformed(ActionEvent e) {
        double snap = AScaleSupport.retrieveSnappedScale(scaleSupport.getMapProjectionOriginScale(-1), AScaleSupport.ZoomOperation.ZOOM_OUT, SCALES);
        if (snap > 0) {
          scaleSupport.setMapProjectionOriginScale(snap, -1);
        }
      }
    };
    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");
  }
}