Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 72 additions & 11 deletions docs/howto/configure-measure-tool.rst
Original file line number Diff line number Diff line change
@@ -1,9 +1,74 @@
How-to configure the Measure tool
=================================

The measure tool is managed as a part of the ServiceManager component.
Therefore, all the options for the measure tool are passed in as options
to the ServiceManger component when it's added to the application.
The measure tool is configured in two places:

#. The application-level ``measure`` configuration object controls which
units are offered and whether the on-map annotations start on or off.
#. The ServiceManager component's ``measureToolOptions`` control the
point-coordinate projections and the initially selected units.

Application measure configuration
---------------------------------

Pass a ``measure`` object when creating the application to control the
units offered in the tool and the default annotation behavior:

.. code:: javascript

var app = new gm3.Application({
mapserver_url: CONFIG.mapserver_url,
mapfile_root: CONFIG.mapfile_root,
measure: {
lengthUnits: ["m", "km", "ft", "mi", "ch", "r"],
areaUnits: ["m", "km", "ft", "mi", "a", "h"],
showMeasureLabels: true
}
});

- ``lengthUnits`` is the list of length units offered in the tool. Valid
values are:

- ``m`` for meters.
- ``km`` for kilometers.
- ``ft`` for feet.
- ``mi`` for miles.
- ``ch`` for chains.
- ``r`` for rods.

The default is ``["m", "km", "ft", "mi", "ch"]``.

- ``areaUnits`` is the list of area units offered in the tool. Valid
values are:

- ``m`` for square meters.
- ``km`` for square kilometers.
- ``ft`` for square feet.
- ``mi`` for square miles.
- ``a`` for acres.
- ``h`` for hectares.

The default is ``["m", "km", "ft", "mi", "a", "h"]``.

- ``showMeasureLabels`` is a boolean that sets the *initial* state of
the on-map measure annotations. ``true`` (the default) shows them,
``false`` hides them until the user turns them on with the labels
button. See :doc:`Turning off measure annotations by default
<disable-measure-labels>` for details.

.. note::

Length and area units are paired: selecting a length unit
automatically selects a complementary area unit (and vice versa).
Chains and rods both pair with acres, following surveyor convention.

ServiceManager options
----------------------

The measure tool is rendered by the ServiceManager component, so the
point-coordinate and initial-unit options are passed in as
``measureToolOptions`` when the ServiceManager is added to the
application.

Here's the example from the demo:

Expand All @@ -20,14 +85,10 @@ Here's the example from the demo:
that is defined the same as with the :doc:`Coordinate
Display <coordinate-display>`. When a user clicks on a point on
the map, the point will be shown in all of the defined projections.
- ``initialUnits`` is one of:
- ``m`` for meters.
- ``km`` for kilometers.
- ``ft`` for feet (this is the default).
- ``mi`` for miles.
- ``ch`` for chains.
- ``a`` for acres.
- ``h`` for hectares.
- ``initialUnits`` overrides the initially selected length and area
units when the tool opens. It is a single unit code from the
``lengthUnits``/``areaUnits`` lists above (for example ``m``, ``ft``,
or ``ch``).

Example of setting the default units to meters
----------------------------------------------
Expand Down
40 changes: 40 additions & 0 deletions docs/howto/disable-measure-labels.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
Turning off measure annotations by default
==========================================

When measuring a line or polygon, GeoMoose annotates each segment with
its length and each polygon with its area directly on the map. These
on-map annotations are **on by default**.

Users can always toggle the annotations on or off while measuring using
the labels button in the upper-left map controls.

To change the *default* so the annotations start **off**, set
``showMeasureLabels`` to ``false`` in the application's ``measure``
configuration object, alongside the other measure options such as
``areaUnits`` and ``lengthUnits``.

Example:

::

var app = new gm3.Application({
mapserver_url: CONFIG.mapserver_url,
mapfile_root: CONFIG.mapfile_root,
measure: {
areaUnits: ["ft", "mi", "a", "h", "m", "km"],
lengthUnits: ["ft", "mi", "ch", "r", "m", "km"],
showMeasureLabels: false
}
});

Valid values:

* ``showMeasureLabels`` - Boolean. ``true`` (default) shows the
annotations when measuring, ``false`` hides them until the user turns
them on with the labels button.

.. note::

This only changes the *initial* state. The labels button still lets
a user turn the annotations back on (or off) at any time while the
measure tool is active.
16 changes: 16 additions & 0 deletions src/gm3/actions/map.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,19 @@ export const setEditPath = createAction("map/set-edit-path");
* @return action definition
*/
export const setEditTools = createAction("map/set-edit-tools");

/* Toggle (or set) the display of the on-map measure annotations.
*
* @param show {Boolean}
*
* @return action definition
*/
export const setShowMeasureLabels = createAction("map/set-show-measure-labels");

/* Set the units the on-map measure annotations are rendered in.
*
* @param units {Object} e.g. { measureLengthUnits: "ft", measureAreaUnits: "ft" }
*
* @return action definition
*/
export const setMeasureUnits = createAction("map/set-measure-units");
42 changes: 42 additions & 0 deletions src/gm3/color.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2026 Dan "Ducky" Little
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

/* Shared color helpers. */

/* Convert a "#rrggbb" color to an [r, g, b, a] array at the given opacity.
*
* @param {String} hexColor A "#rrggbb" color string.
* @param {Number} opacity Alpha from 0 to 1.
*
* @returns {Array} [r, g, b, a] suitable for an OpenLayers fill/stroke color.
*/
export const withOpacity = (hexColor, opacity) => {
const hex = hexColor.replace("#", "");
return [
parseInt(hex.substring(0, 2), 16),
parseInt(hex.substring(2, 4), 16),
parseInt(hex.substring(4, 6), 16),
opacity,
];
};
6 changes: 4 additions & 2 deletions src/gm3/components/map/button.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import React from "react";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";

const MapButton = ({ label, icon, onClick, index, disabled }) => {
const MapButton = ({ label, icon, onClick, index, disabled, active }) => {
const { t } = useTranslation();
return (
<button
disabled={disabled}
className={`map-button fade-in ${index}`}
className={`map-button fade-in ${index}${active ? " active" : ""}`}
title={t(label)}
onClick={onClick}
>
Expand All @@ -23,6 +23,7 @@ MapButton.defaultProps = {
t: (r) => r,
index: 0,
disabled: false,
active: false,
};

MapButton.propTypes = {
Expand All @@ -31,6 +32,7 @@ MapButton.propTypes = {
onClick: PropTypes.func,
index: PropTypes.number,
disabled: PropTypes.bool,
active: PropTypes.bool,
};

export default MapButton;
2 changes: 2 additions & 0 deletions src/gm3/components/map/context-controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ const ContextControls = ({
setEditTools,
zoom,
resolution,
children,
}) => {
let controls = false;
// do not bother rendering anything if the interaction is null
Expand Down Expand Up @@ -211,6 +212,7 @@ const ContextControls = ({

<span style={{ display: "inline-block", width: 16 }}></span>
{controls}
{children}
</React.Fragment>
);
};
Expand Down
74 changes: 71 additions & 3 deletions src/gm3/components/map/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import {
createLayer as createMeasureLayer,
updateLayer as updateMeasureLayer,
} from "./layers/measure";
import MapButton from "./button";

import { wfsGetFeatures } from "./layers/wfs";
import { EDIT_LAYER_NAME } from "../../defaults";
Expand Down Expand Up @@ -135,6 +136,23 @@ class Map extends React.Component {

// this is used when a feature isn't finished yet.
this.sketchFeature = null;

// bound so it can be handed to the OL style functions and read the
// latest config on every render.
this.getMeasureLabelOptions = this.getMeasureLabelOptions.bind(this);
}

/** Current on-map measure-label options, read live from the map state.
*
* @returns {Object} `{ enabled, lengthUnits, areaUnits }`
*/
getMeasureLabelOptions() {
const mapView = this.props.mapView;
return {
enabled: mapView.showMeasureLabels === true,
lengthUnits: mapView.measureLengthUnits || "ft",
areaUnits: mapView.measureAreaUnits || "ft",
};
}

/** Update a source's important bits.
Expand All @@ -156,7 +174,7 @@ class Map extends React.Component {
agsLayer.updateLayer(this.map, olLayer, mapSource);
break;
case "measure":
updateMeasureLayer(this.map, olLayer, mapSource);
updateMeasureLayer(this.map, olLayer, mapSource, undefined, this.getMeasureLabelOptions);
break;
case "vector":
case "wfs":
Expand Down Expand Up @@ -196,7 +214,7 @@ class Map extends React.Component {
case "ags":
return agsLayer.createLayer(mapSource);
case "measure":
return createMeasureLayer(mapSource);
return createMeasureLayer(mapSource, this.getMeasureLabelOptions);
case "vector":
case "wfs":
case "ags-vector":
Expand Down Expand Up @@ -404,6 +422,14 @@ class Map extends React.Component {
// create the selection layer.
this.configureSelectionLayer();

// Measure annotations are on by default (opt-out). Honor a deployer's
// config.measure.showMeasureLabels override here -- it sits alongside the
// other measure options (areaUnits, lengthUnits). The map mounts once, so
// this seeds the initial state without clobbering later user toggles.
if (typeof this.props.measureConfig.showMeasureLabels === "boolean") {
this.props.setShowMeasureLabels(this.props.measureConfig.showMeasureLabels);
}

const viewParams = {};

if (this.props.center) {
Expand Down Expand Up @@ -764,6 +790,28 @@ class Map extends React.Component {
* cycle where the state can get modified.
*/
componentDidUpdate(prevProps) {
// When the measure label options change (the toggle or the selected
// units), force the measure layer to restyle. These changes do not
// bump the layer's featuresVersion, so a manual redraw is needed.
const mapView = this.props.mapView;
const prevMapView = prevProps.mapView;
if (
mapView.showMeasureLabels !== prevMapView.showMeasureLabels ||
mapView.measureLengthUnits !== prevMapView.measureLengthUnits ||
mapView.measureAreaUnits !== prevMapView.measureAreaUnits
) {
const measureLayer = this.olLayers["measure"];
if (measureLayer) {
updateMeasureLayer(
this.map,
measureLayer,
this.props.mapSources["measure"],
undefined,
this.getMeasureLabelOptions
);
}
}

// extent takes precedent over the regular map-view,
if (this.props.mapView.extent) {
this.zoomToExtent(this.props.mapView.extent);
Expand Down Expand Up @@ -865,6 +913,14 @@ class Map extends React.Component {

const enableZoomJump = config.enableZoomJump === true;

// the measure-label toggle is relevant when the measure service is active
// (rather than the active draw source, which clears on "stop") or while
// there are measure features lingering on the map.
const measureSource = this.props.mapSources.measure;
const measuring =
this.props.serviceName === "measure" || (measureSource && measureSource.features.length > 0);
const showMeasureLabels = this.props.mapView.showMeasureLabels === true;

return (
<div
className="map"
Expand Down Expand Up @@ -905,7 +961,16 @@ class Map extends React.Component {
showZoom={config.showZoom === true}
setEditPath={this.props.setEditPath}
setEditTools={this.props.setEditTools}
/>
>
{measuring && (
<MapButton
label={showMeasureLabels ? "measure-hide-labels" : "measure-show-labels"}
icon="icon labels"
active={showMeasureLabels}
onClick={() => this.props.setShowMeasureLabels(!showMeasureLabels)}
/>
)}
</ContextControls>
</div>
</div>
);
Expand All @@ -921,7 +986,9 @@ function mapState(state) {
return {
mapSources: state.mapSources,
mapView: state.map,
serviceName: state.query.serviceName,
config: state.config.map || {},
measureConfig: state.config.measure || {},
selectionStyle: state.config.selectionStyle || {},
// resolve this to meters
selectionBuffer: util.convertLength(
Expand Down Expand Up @@ -958,6 +1025,7 @@ function mapDispatch(dispatch) {
dispatch(mapSourceActions.saveFeature(path, feature));
},
setZoom: (z) => dispatch(mapActions.setView({ zoom: z })),
setShowMeasureLabels: (show) => dispatch(mapActions.setShowMeasureLabels(show)),
removeFeature: (path, feature) => {
dispatch(removeFeature(path, feature));
},
Expand Down
Loading
Loading