Distance scale indicator

@Russell_Grew or anyone else,

I’m looking to implement a distance scale indicator. Does anyone have sample code or something in the Showcase to do this?

I would like to implement exactly what this site has https://nationalmap.gov.au/

image

But I did not see where this was implemented when I looked at the provided repository. https://github.com/TerriaJS/TerriaMap/

1 Like

I think the easiest way for you might be to use TerriaJS itself as a library, they have a getting started guide here: https://github.com/TerriaJS/TerriaJS#getting-started

Otherwise you’ll need to find the source code for this in TerriaJS, extract it, and then implement it in your CesiumJS app.

I believe you can now find the code here
github.com/TerriaJS/terriajs/blob/2a395fe45ca239c8d2110f4c0f0474a3efcf8b9e/lib/ReactViews/Map/Legend/DistanceLegend.jsx

There was another thread about this(5 years ago)
2d scale implementation for Legend?

@Hyper_Sonic Yes, I’ve seen the previous threads. The moderator asked me to start my own thread since the others had been several years ago.

I’ve also seen that DistanceLegend.jsx file before. Do you know how to implement this without having to use the Terria library? I was hoping someone had something set up in the showcase.

I believe the gist of it is here

This shoots out 2 rays 1 pixel apart in the screen space. It then determines where they land on Earth, and determines the distance between those 2 landing spots, can call this distance pixelDistance. This lets you know how big a 2D screen pixel is when laid over the 3D view. Say pixelDistance is 10 meters, then a bar 10 pixels long would be a scale of 100 meters.

If the camera is very high though, having one scale bar would not be sufficient, due to the curvature of the Earth (1 pixel on the screen edge would represent a longer distance than on the screen center.)

1 Like

I started following the getting started guide on TerriaJS. My npm install command failed because it could not run the post install script.

I tried running using GITBash and Cygwin but failed:
WARN react-datepicker@0.53.0 requires a peer of react@^0.14.0 || ^15.0.0 but none is installed. You must install peer dependencies yourself.
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! terriajs@7.11.6 postinstall: gulp post-npm-install
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the terriajs@7.11.6 postinstall script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

Not sure how to proceed. Now it seems I’m jumping down a rabbit hole that I was worried about when I first was asking this question on here.

https://docs.terria.io/guide/getting-started/ talks about the need to use Git Bash if you are running windows.

I think also that this may not be best the place for detailed Terria discussions.

Maybe I’m going about this the wrong way. Let me start simple, that way I can understand the pieces and parts a little at a time.

In the picture, is that box an infoBox? If so, how do you show whatever widget this is at the bottom right of the screen?

That’s just an HTML element added on the page on top of the CesiumJS canvas. So you’d need to create this HTML element, position it to the bottom (or top right etc) with CSS, and then update its contents whenever the CesiumJS camera changes.

At the bottom
github.com/TerriaJS/terriajs/blob/next/lib/ReactViews/Map/Legend/DistanceLegend.jsx#L202

barWidth determines width of barStyle. That box is a div, which contains a label and another div. That other div displays the bar using className and style properties. Not sure, but that probably translates to
https://www.w3schools.com/w3css/w3css_progressbar.asp
if not SVG graphics. Progress bars can be set by pixels or % for either width or height.

For anyone else interested, I have a simple solution without having to use terriaJS. I did use some of the calculations from the provided file above DistanceLegend.jsx.

html file:

<!DOCTYPE html>
<html lang="en">
<head>
  <link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">

  <style>
     #scaleBox
     {
        padding: 10px 10px;
        bottom: 35px;
        right: 10px;
        position: absolute;
        display: inline-block;
        background: #304458;
        color: #bacdcc;
        border-radius: 5px;
        font-family: Arial;
        font-size: 14px;
        font-style: normal;
        text-align: center;
        white-space:pre-wrap;
     }
     
     .scale-text
     {
        display: inline-block;
        position: relative;
        background: #304458;
        color: #bacdcc;
        border-radius: 2px;
        padding: 0px 0px;
        margin: 0px 0px;
        font-family: Arial;
        font-size: 14px;
        font-style: normal;
        text-align: center;
        white-space:pre-wrap;
     }
     
     .scalebar
     {
        height:6px;
        width:100px;
        padding-left: 5%
        margin-top: 10px;
        margin-left: auto;
        margin-right: auto;
     }
  </style>
  </head>
<body>

  <div id="scaleBox">
    <div id="scalebar" class="w3-container w3-grey scalebar"></div>
    <p id="scalebartag" class="scale-text">Distance Scale</p>
  </div>
  <script src="./myScript.js"></script>
</body>
</html>

js myScript file:

//Zoom listener
viewer.camera.moveEnd.addEventListener(function() { 
   // the camera stopped moving
   updateDistanceScale();
});

function updateDistanceScale()
{
    var geodesic = new Cesium.EllipsoidGeodesic();
    var distances = [
        1, 2, 3, 5,
        10, 20, 30, 50,
        100, 200, 300, 500,
        1000, 2000, 3000, 5000,
        10000, 20000, 30000, 50000,
        100000, 200000, 300000, 500000,
        1000000, 2000000, 3000000, 5000000,
        10000000, 20000000, 30000000, 50000000];

     // Find the distance between two pixels at the bottom center of the screen.
     var width = viewer.scene.canvas.clientWidth;
     var height = viewer.scene.canvas.clientHeight;

     var left = viewer.scene.camera.getPickRay(new Cesium.Cartesian2((width / 2) | 0, height - 1));
     var right = viewer.scene.camera.getPickRay(new Cesium.Cartesian2(1 + (width / 2) | 0, height - 1));
 
     var globe = viewer.scene.globe;
     var leftPosition = globe.pick(left, viewer.scene);
     var rightPosition = globe.pick(right, viewer.scene);

     if (typeof leftPosition == "undefined" || typeof rightPosition == "undefined") {
         $("#scalebartag").text("undefined");
         return;
    }
 
    var leftCartographic = globe.ellipsoid.cartesianToCartographic(leftPosition);
    var rightCartographic = globe.ellipsoid.cartesianToCartographic(rightPosition);

    geodesic.setEndPoints(leftCartographic, rightCartographic);
    var pixelDistance = geodesic.surfaceDistance * 3.28084; //meters to feet
    var pixelDistanceMiles = pixelDistance / 5280;

    // Find the first distance that makes the scale bar less than 100 pixels.
    var maxBarWidth = 100;
    var distance;
    var label;
    var units;

    for (var i = distances.length - 1; i >= 0; --i) {
         if (distances[i] / pixelDistance < maxBarWidth) {
            if(distances[i] > 5280)
            {
                for (var j = distances.length - 1; j >= 0; --j)
                {
                    if (distances[j] / pixelDistanceMiles < maxBarWidth)
                    {
                        distance = distances[j];
                        units = " mi";
                        break;
                    }
                 }
                break;
            }
            else
            {
                distance = distances[i];
                units = " ft";
                break;
            }  
        }
    }

    if (typeof distance !== "undefined") {

        label = distance.toString() + units;

        if(units === " mi")
        {
            document.getElementById("scalebar").style.width = ((distance / pixelDistanceMiles) | 0).toString() + "px";
        }
        else
        {
            document.getElementById("scalebar").style.width = ((distance / pixelDistance) | 0).toString() + "px";
        }
    
        $("#scalebartag").text(label);
    } else {
        document.getElementById("scalebar").style.width = "100px";
        $("#scalebartag").text("undefined");
     }
}

image

3 Likes

Glad you got it working. So instead of a progress bar you just change the size of the div itself? Apparently the bitwise OR is used as a Math.floor shortcut.

An alternative to writing out the long array

var i=100000000;var j=1;
function next()
{
    if(j==4){i*=3/5;j=3;return i;}
    if(j==3){i*=2/3;j=2;return i;}
    if(j==2){i*=1/2;j=1;return i;}
    if(j==1){i*=1/2;j=4;return i;}
}

Yeah, I looked at the progress bar examples and thought it would seem to work fine with just using the div.

I like the alternative code you propose. The original array I just got from the terriaJS example. If you want to post an update to the entire function, be my guest. I’d like to see how you thought it would get integrated.

I haven’t tested this, but it should work. Instead of having an ‘o2’ you could instead just reset the values of ‘o1’ (at first I thought they’d be used simultaneously)

var o1={i:100000000,j:1};
var o2={i:100000000,j:1};
function next(in)
{
    if(in.j==4){in.i*=3/5;in.j=3;return in.i;}
    if(in.j==3){in.i*=2/3;in.j=2;return in.i;}
    if(in.j==2){in.i*=1/2;in.j=1;return in.i;}
    if(in.j==1){in.i*=1/2;in.j=4;return in.i;}
}

while ((d=next(o1))>=1) { //maybe use less parenthesis, I think '=' has precedence over '>='
     if (d / pixelDistance < maxBarWidth) {
        if(d > 5280)
        {
            while ((d=next(o2))>=1)
            {
                if (d / pixelDistanceMiles < maxBarWidth)
                {
                    distance = d;
                    units = " mi";
                    break;
                }
             }
            break;
        }
        else
        {
            distance = d;
            units = " ft";
            break;
        }
    }
}
1 Like

thank you for this! you saved me from going nuts :sweat_smile: