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
39 changes: 39 additions & 0 deletions actions/bookmark.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* Copyright 2025 Stadtwerke München GmbH
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/

import axios from 'axios';

import bookmarkReducer from '../reducers/bookmark';
import ReducerIndex from '../reducers/index';
import ConfigUtils from '../utils/ConfigUtils';
ReducerIndex.register("bookmark", bookmarkReducer);

export const SET_BOOKMARKS = 'SET_BOOKMARKS';

export function setBookmarks(bookmarks) {
return {
type: SET_BOOKMARKS,
bookmarks
};
}

export function refreshUserBookmarks() {
return (dispatch, getState) => {
const username = ConfigUtils.getConfigProp("username");
const permalinkServiceUrl = ConfigUtils.getConfigProp("permalinkServiceUrl");
if (username && permalinkServiceUrl) {
axios.get(permalinkServiceUrl.replace(/\/$/, '') + "/bookmarks/")
.then(response => {
dispatch(setBookmarks(response.data || []));
})
.catch(() => {
dispatch(setBookmarks([]));
});
}
};
}
2 changes: 2 additions & 0 deletions appConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import HeightProfilePlugin from './plugins/HeightProfile';
import HelpPlugin from './plugins/Help';
import HomeButtonPlugin from './plugins/HomeButton';
import IdentifyPlugin from './plugins/Identify';
import LayerBookmarkPlugin from './plugins/LayerBookmark';
import LayerCatalogPlugin from './plugins/LayerCatalog';
import LayerTreePlugin from './plugins/LayerTree';
import LocateButtonPlugin from './plugins/LocateButton';
Expand Down Expand Up @@ -89,6 +90,7 @@ export default {
AuthenticationPlugin: AuthenticationPlugin,
BackgroundSwitcherPlugin: BackgroundSwitcherPlugin,
BookmarkPlugin: BookmarkPlugin,
LayerBookmarkPlugin: LayerBookmarkPlugin,
BottomBarPlugin: BottomBarPlugin,
CookiePopupPlugin: CookiePopupPlugin,
CyclomediaPlugin: CyclomediaPlugin,
Expand Down
142 changes: 142 additions & 0 deletions components/BookmarkPanel.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/**
* Copyright 2021 Oslandia SAS <[email protected]>
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/

import React from 'react';

import classnames from 'classnames';
import isEmpty from 'lodash.isempty';
import PropTypes from 'prop-types';

import ConfigUtils from '../utils/ConfigUtils';
import Icon from './Icon';
import Spinner from './widgets/Spinner';

import '../plugins/style/Bookmark.css';


/**
* Reusable panel component for managing bookmarks.
*
* Used in both Bookmark and LayerBookmark plugins.
*/
export default class BookmarkPanel extends React.Component {
static propTypes = {
bookmarks: PropTypes.array,
onAdd: PropTypes.func,
onOpen: PropTypes.func,
onRemove: PropTypes.func,
onUpdate: PropTypes.func,
onZoomToExtent: PropTypes.func,
translations: PropTypes.objectOf(PropTypes.string)
};
state = {
currentBookmark: null,
description: "",
adding: false,
saving: false,
trashing: false
};
componentDidUpdate(prevProps) {
if (prevProps.bookmarks !== this.props.bookmarks) {
this.setState({adding: false, saving: false, trashing: false});
// Select a recently added bookmark
const addedBookmark = this.props.bookmarks.find(bookmark =>
!prevProps.bookmarks.some(prevBookmark => prevBookmark.key === bookmark.key)
);
if (addedBookmark) {
this.setState({
currentBookmark: addedBookmark.key,
description: addedBookmark.description
});
}
}
}
render() {
const username = ConfigUtils.getConfigProp("username");
const currentBookmark = this.props.bookmarks.find(bookmark => bookmark.key === this.state.currentBookmark);

return (
<div className="bookmark-body" role="body">
{!username ? (
this.props.translations?.notloggedin
) : (
<>
<h4>{this.props.translations?.manage}</h4>
<div className="bookmark-create">
<input onChange={ev => this.setState({description: ev.target.value})}
onKeyDown={ev => {if (ev.key === "Enter" && this.state.description !== "") { this.addBookmark(); }}}
placeholder={this.props.translations?.description} type="text" value={this.state.description} />
</div>
<div className="bookmark-actions controlgroup">
<button className="button" disabled={!currentBookmark} onClick={() => this.props.onOpen(currentBookmark.key, false)} title={this.props.translations?.open}>
<Icon icon="folder-open" />
</button>
<button className="button" disabled={!currentBookmark} onClick={() => this.props.onOpen(currentBookmark.key, true)} title={this.props.translations?.openTab}>
<Icon icon="open_link" />
</button>
{this.props.onZoomToExtent ? (
<button className="button" disabled={!currentBookmark} onClick={() => this.props.onZoomToExtent(currentBookmark.key)} title={this.props.translations?.zoomToExtent}>
<Icon icon="zoom" />
</button>
) : null}
<span className="bookmark-actions-spacer" />
<button className="button" disabled={!this.state.description} onClick={this.addBookmark} title={this.props.translations?.add}>
{this.state.adding ? (<Spinner />) : (<Icon icon="plus" />)}
</button>
<button className="button" disabled={!currentBookmark || !this.state.description} onClick={() => this.updateBookmark(currentBookmark)} title={this.props.translations?.update}>
{this.state.saving ? (<Spinner />) : (<Icon icon="save" />)}
</button>
<button className="button" disabled={!currentBookmark} onClick={() => this.removeBookmark(currentBookmark)} title={this.props.translations?.remove}>
{this.state.trashing ? (<Spinner />) : (<Icon icon="trash" />)}
</button>
</div>
<div className="bookmark-list">
{this.props.bookmarks.map((bookmark) => {
const itemclasses = classnames({
"bookmark-list-item": true,
"bookmark-list-item-active": this.state.currentBookmark === bookmark.key
});
return (
<div className={itemclasses} key={bookmark.key}
onClick={() => this.toggleCurrentBookmark(bookmark)}
onDoubleClick={() => this.props.onOpen(bookmark.key, false)}
title={this.props.translations?.lastUpdate + ": " + bookmark.date}
>
{bookmark.description}
</div>
);
})}
{isEmpty(this.props.bookmarks) ? (
<div className="bookmark-list-item-empty">{this.props.translations?.nobookmarks}</div>
) : null}
</div>
</>
)}
</div>
);
}
toggleCurrentBookmark = (bookmark) => {
if (this.state.currentBookmark === bookmark.key) {
this.setState({currentBookmark: null, description: ""});
} else {
this.setState({currentBookmark: bookmark.key, description: bookmark.description});
}
};
addBookmark = () => {
this.setState({adding: true, description: "", currentBookmark: null});
this.props.onAdd(this.state.description);
};
updateBookmark = (bookmark) => {
this.setState({saving: true});
this.props.onUpdate(bookmark.key, this.state.description);
};
removeBookmark = (bookmark) => {
this.setState({trashing: true, description: "", currentBookmark: null});
this.props.onRemove(bookmark.key);
};
}
4 changes: 4 additions & 0 deletions components/StandardApp.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {register as olProj4Register} from 'ol/proj/proj4';
import Proj4js from 'proj4';
import PropTypes from 'prop-types';

import {refreshUserBookmarks} from '../actions/bookmark';
import {localConfigLoaded, setStartupParameters, setColorScheme} from '../actions/localConfig';
import {changeLocale} from '../actions/locale';
import {setCurrentTask} from '../actions/task';
Expand Down Expand Up @@ -303,6 +304,9 @@ export default class StandardApp extends React.Component {
const colorScheme = initialParams.style || storedColorScheme || ConfigUtils.getConfigProp("defaultColorScheme");
StandardApp.store.dispatch(setColorScheme(colorScheme));

// Load all bookmarks
StandardApp.store.dispatch(refreshUserBookmarks());

// Resolve permalink and restore settings
resolvePermaLink(initialParams, (params, state, success) => {
StandardApp.store.dispatch(setStartupParameters(params, state));
Expand Down
56 changes: 56 additions & 0 deletions components/widgets/GroupSelect.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* Copyright 2025 Stadtwerke München GmbH
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/

import React, {Component} from 'react';

import PropTypes from 'prop-types';

/**
* Dropdown for selecting options grouped under sections.
*/
export default class GroupSelect extends Component {
static propTypes = {
defaultOption: PropTypes.array,
onChange: PropTypes.func,
options: PropTypes.object,
placeholder: PropTypes.string,
value: PropTypes.string
};
static defaultProps = {
defaultOption: null,
placeholder: null
};
render() {
return (
<select onChange={this.onChange} role="input" value={this.props.value}>
{this.props.placeholder !== null ? (
<option disabled hidden selected>
{this.props.placeholder}
</option>
) : null}
{this.props.defaultOption !== null ? (
<option key={this.props.defaultOption[0]} value={this.props.defaultOption[0]}>
{this.props.defaultOption[1]}
</option>
) : null}
{Object.entries(this.props.options || {}).map(([title, options], index) => (
options && options.length > 0 ? (
<optgroup key={"optgroup-" + index} label={title}>
{options.map(([value, description]) => (
<option key={value} value={value}>{description}</option>
))}
</optgroup>
) : null
))}
</select>
);
}
onChange = (e) => {
this.props.onChange(e.target.value);
};
}
16 changes: 15 additions & 1 deletion doc/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Plugin reference
* [Help](#help)
* [HomeButton](#homebutton)
* [Identify](#identify)
* [LayerBookmark](#layerbookmark)
* [LayerCatalog](#layercatalog)
* [LayerTree](#layertree)
* [LocateButton](#locatebutton)
Expand Down Expand Up @@ -288,7 +289,7 @@ Map button for switching the background layer.

Bookmark<a name="bookmark"></a>
----------------------------------------------------------------
Allows managing user bookmarks.
Allows managing user bookmarks which are storing the current view, including the location and zoom level.

Bookmarks are only allowed for authenticated users.

Expand All @@ -306,6 +307,7 @@ Bottom bar, displaying mouse coordinate, scale, etc.
|----------|------|-------------|---------------|
| additionalBottomBarLinks | `[{`<br />`  label: string,`<br />`  labelMsgId: string,`<br />`  side: string,`<br />`  url: string,`<br />`  urlTarget: string,`<br />`  icon: string,`<br />`}]` | Additional bottombar links.`side` can be `left` or `right` (default). | `undefined` |
| coordinateFormatter | `func` | Custom coordinate formatter, as `(coordinate, crs) => string`. | `undefined` |
| displayBookmarkDropdown | `string` | Whether to display a dropdown menu for the selection of user bookmarks in the bottom bar. Possible values are `none`, `all`, `withPosition` and `withoutPosition`. | `'none'` |
| displayCoordinates | `bool` | Whether to display the coordinates in the bottom bar. | `true` |
| displayScalebar | `bool` | Whether to display the scalebar in the bottom bar. | `true` |
| displayScales | `bool` | Whether to display the scale in the bottom bar. | `true` |
Expand Down Expand Up @@ -498,6 +500,18 @@ for customized queries and templates for the result presentation.
| resultDisplayMode | `string` | Result display mode, one of `tree`, `flat`, `paginated`. | `'flat'` |
| showLayerSelector | `bool` | Whether to show a layer selector to filter the identify results by layer. | `true` |

LayerBookmark<a name="layerbookmark"></a>
----------------------------------------------------------------
Allows managing layer bookmarks which are storing the currently visible layers without the location and zoom level.

Bookmarks are only allowed for authenticated users.

Requires `permalinkServiceUrl` to point to a `qwc-permalink-service`.

| Property | Type | Description | Default value |
|----------|------|-------------|---------------|
| side | `string` | The side of the application on which to display the sidebar. | `'right'` |

LayerCatalog<a name="layercatalog"></a>
----------------------------------------------------------------
Displays a pre-configured catalog of external layers in a window.
Expand Down
Loading
Loading