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
27 changes: 27 additions & 0 deletions examples/desktop/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,33 @@ app.loadMapbook().then(function() {

app.add(gm3.components.Map, 'map', {});

var lmicPreviewUrl = CONFIG.mapserver_url
+ '?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&FORMAT=image%2Fjpeg'
+ '&LAYERS=mncomp'
+ '&MAP=' + encodeURIComponent(CONFIG.mapfile_root + 'demo/wms/wms_proxy.map')
+ '&SRS=EPSG%3A3857&STYLES=&WIDTH=40&HEIGHT=40'
+ '&BBOX=-10389135.541557081%2C5535452.120985681%2C-10351566.742154919%2C5566447.33595532';

app.add(gm3.components.BasemapToggle, 'basemap-toggle', {
layers: [
{
label: 'No background',
src: 'data:image/gif;base64,R0lGODlhAQABAIAAAP7//wAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==',
path: 'blank/blank',
},
{
label: 'OpenStreetMap',
src: 'https://a.tile.openstreetmap.org/12/989/1480.png',
path: 'openstreetmap/osm_mapnik',
},
{
label: 'Aerial',
src: lmicPreviewUrl,
path: 'lmic/mncomp',
}
]
});

var print_preview = app.add(gm3.components.PrintModal, 'print-preview', {});
app.registerAction('print', function() {
this.run = function() {
Expand Down
1 change: 1 addition & 0 deletions examples/desktop/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
<div id="map-pane">
<div id="map-container">
<div id="map"></div>
<div id="basemap-toggle"></div>
</div>
<div id="results-grid"></div>
</div>
Expand Down
2 changes: 2 additions & 0 deletions src/gm3/actions/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,5 @@ export const clearAction = createAction("ui/clear-action");
export const showModal = createAction("ui/show-modal");

export const hideModal = createAction("ui/hide-modal");

export const setMapbookReady = createAction("ui/set-mapbook-ready");
4 changes: 4 additions & 0 deletions src/gm3/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,8 @@ class Application {
}

populateMapbook(contents) {
this.store.dispatch(uiActions.setMapbookReady(false));

let mapbookXml = contents;
if (typeof contents === "string") {
mapbookXml = new DOMParser().parseFromString(contents, "text/xml");
Expand Down Expand Up @@ -350,6 +352,8 @@ class Application {
this.store.dispatch(action)
);

this.store.dispatch(uiActions.setMapbookReady(true));

// return the parsed version of the document.
return mapbookXml;
}
Expand Down
181 changes: 181 additions & 0 deletions src/gm3/components/basemap-toggle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/*
* Copyright (c) 2016-2026 Dan "Ducky" Little & GeoMoose.org
*
* 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.
*/

import React, { useCallback, useMemo, useState } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";

import { setLayerVisibility } from "../actions/mapSource";
import { getLayerName, getMapSourceName, isLayerOn } from "../util";

function getLayerOnState(mapSources, path) {
return isLayerOn(mapSources, {
src: [
{
mapSourceName: getMapSourceName(path),
layerName: getLayerName(path),
},
],
});
}

function getActiveLayerIndex(layers, mapSources) {
for (let i = 0, ii = layers.length; i < ii; i++) {
if (layers[i].path !== "" && getLayerOnState(mapSources, layers[i].path)) {
return i;
}
}
return -1;
}

const BasemapToggleChip = ({ active, open, path, src, label, onClick }) => {
return (
<button
type="button"
key={path || label}
onClick={onClick}
aria-pressed={active}
className={`basemap-toggle-chip ${open ? "open" : ""} ${active ? "active" : ""}`}
>
<img src={src} alt="" className="basemap-toggle-image" />
<span className="basemap-toggle-label">{label}</span>
</button>
);
};

BasemapToggleChip.propTypes = {
active: PropTypes.bool,
open: PropTypes.bool,
path: PropTypes.string,
src: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
onClick: PropTypes.func,
};

BasemapToggleChip.defaultProps = {
active: false,
open: false,
path: "",
onClick: () => {},
};

const BasemapToggleComponent = ({ layers, mapSources, mapbookReady, onSetLayerVisibility }) => {
const [isOpen, setOpen] = useState(false);
const activeIndex = useMemo(() => getActiveLayerIndex(layers, mapSources), [layers, mapSources]);
const setVis = onSetLayerVisibility;

const handleLayerClick = useCallback(
(layer) => {
setVis(layer.path, true);
layers.forEach((offLayer) => {
if (offLayer.path !== layer.path) {
setVis(offLayer.path, false);
}
});
},
[layers, setVis]
);

if (!mapbookReady) {
return null;
}

// This happens, generally, during a misconfiguration::
// - The admin configures the base layers to work "in a group"
// - The user clicks a layer that is in a group where the rest of the configured basemaps are "off"
// This means there is no placeholder for that layer in the basemap toggle!
//
if (activeIndex < 0) {
console.warn("Basemap toggle configuration error! All exclusive layers are off.");

// short circuit the rendering
return (
<div className="basemap-toggle error">
<span
className="error-indicator"
title="Invalid basemap toggle state: choose a different base layer"
>
{/* unicode warning symbol */ "\u26A0"}
</span>
</div>
);
}

return (
<div
onMouseEnter={() => setOpen(true)}
onMouseLeave={() => setOpen(false)}
onFocus={() => setOpen(true)}
onBlur={(evt) => {
if (!evt.currentTarget.contains(evt.relatedTarget)) {
setOpen(false);
}
}}
// When there is no active index "fold" the basemap chooser down
// to prevent being in an indeterminate state.
className={`basemap-toggle ${isOpen ? "full-open" : ""}`}
>
{layers.map((layer, idx) => (
<BasemapToggleChip
key={layer.path || idx}
label={layer.label}
src={layer.src}
path={layer.path}
active={idx === activeIndex}
open={isOpen || idx === activeIndex}
onClick={() => handleLayerClick(layer)}
/>
))}
</div>
);
};

BasemapToggleComponent.propTypes = {
layers: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.string.isRequired,
src: PropTypes.string.isRequired,
path: PropTypes.string.isRequired,
})
),
mapSources: PropTypes.object.isRequired,
mapbookReady: PropTypes.bool.isRequired,
onSetLayerVisibility: PropTypes.func.isRequired,
};

BasemapToggleComponent.defaultProps = {
layers: [],
};

export const mapStateToProps = (state) => ({
mapSources: state.mapSources,
mapbookReady: state.ui.mapbookReady,
});

export const mapDispatchToProps = (dispatch) => ({
onSetLayerVisibility: (path, on) =>
dispatch(setLayerVisibility(getMapSourceName(path), getLayerName(path), on)),
});

export default connect(mapStateToProps, mapDispatchToProps)(BasemapToggleComponent);

export { BasemapToggleChip, BasemapToggleComponent };
5 changes: 5 additions & 0 deletions src/gm3/reducers/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,15 @@ import {
clearAction,
showModal,
hideModal,
setMapbookReady,
} from "../actions/ui";

const defaultState = {
stateId: 0,
hint: null,
action: null,
modal: "",
mapbookReady: false,
};

const reducer = createReducer(defaultState, {
Expand All @@ -67,6 +69,9 @@ const reducer = createReducer(defaultState, {
[hideModal]: (state) => {
state.modal = "";
},
[setMapbookReady]: (state, { payload }) => {
state.mapbookReady = payload;
},
});

export default reducer;
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import MeasureTool from "./gm3/components/measure";
import PrintModal from "./gm3/components/print/printModal";
import JumpToExtent from "./gm3/components/jumpToExtent";
import BookmarkModal from "./gm3/components/bookmark-modal";
import BasemapToggle from "./gm3/components/basemap-toggle";

import LocalStorageTracker from "./gm3/trackers/localStorage";
import HashTracker from "./gm3/trackers/hash";
Expand All @@ -67,6 +68,7 @@ var components = {
PrintModal: PrintModal,
JumpToExtent: JumpToExtent,
BookmarkModal,
BasemapToggle,
};

var trackers = {
Expand Down
76 changes: 76 additions & 0 deletions src/less/basemap-toggle.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
.basemap-toggle {
position: absolute;
bottom: 30px;
right: 10px;
z-index: 100;
border-radius: 5px;
background: rgba(0, 0, 0, 0.4);
color: white;
padding: 5px;
display: flex;

&.full-open {
.basemap-toggle-chip {
margin-left: 5px;
margin-right: 5px;
&.active {
background-color: rgba(123, 178, 116, 0.75);
}
}
}

&.error {
background: rgba(255, 0, 0, 0.4);
}
}

.basemap-toggle-chip {
display: flex;
overflow: hidden;
flex-direction: column;
cursor: pointer;
transition:
width 1s,
margin 1s,
background-color 0.2s;
width: 0;
margin: 0;

align-items: center;
justify-content: center;
border-radius: 5px;
border: 0;
padding: 0;
appearance: none;
background: transparent;
color: inherit;
font: inherit;

img {
border-radius: 5px;
}

&:focus-visible {
outline: 2px solid white;
outline-offset: 2px;
}

&.open {
width: 100px;
}
}

.basemap-toggle-image {
display: block;
width: 100px;
height: 100px;
}

.basemap-toggle-label {
text-align: center;
margin: 0;
flex: 1;
width: 115px;
font-size: 10px;
font-weight: bold;
}
1 change: 1 addition & 0 deletions src/less/gm3.less
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
@import "toolbar.less";
@import "attribution-display.less";
@import "map.less";
@import "basemap-toggle.less";
@import "editor.less";
@import "progress.less";

Expand Down
Loading
Loading