Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tests, docs, build #266

Draft
wants to merge 59 commits into
base: master
Choose a base branch
from
Draft
Changes from 1 commit
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
aa2d926
Account for file created by yarn install in gitignore
TNick Oct 12, 2023
924966d
Merge remote-tracking branch 'upstream/master'
TNick Oct 13, 2023
fe556a4
Merge branch 'master' of https://github.com/qgis/qwc2
TNick Oct 13, 2023
2859428
Initial configuration for building a library, tests and documentation
TNick Oct 14, 2023
1008a4d
Tests for all functions in CoordinatesUtils
TNick Oct 15, 2023
76fbe43
rimraf is dev dependency
TNick Oct 15, 2023
b65ed8a
Document some actions and store components
TNick Oct 15, 2023
810a01f
Add typescript
TNick Oct 15, 2023
baae1e0
Move to typedoc
TNick Oct 15, 2023
7c26543
Add some definitions (store-related), integrate them into source file…
TNick Oct 15, 2023
8592688
Add documentation and types for measurement-related code
TNick Oct 16, 2023
6925f02
Measurement utils test
TNick Oct 17, 2023
01b9727
Locale utils tests and docs
TNick Oct 17, 2023
6852ca7
map utils tests
TNick Oct 18, 2023
00e7314
Merge branch 'master' of https://github.com/qgis/qwc2 into add_tests_…
TNick Oct 18, 2023
742792a
Misc utils tests
TNick Oct 18, 2023
7460fce
Forgot some comments in source code
TNick Oct 18, 2023
84d046d
Signal tests
TNick Oct 19, 2023
a9cbfe8
distinct setup files (global and after-env)
TNick Oct 20, 2023
36ba6d9
ConfigUtils tests
TNick Oct 20, 2023
b27ec1b
properly exclude test files from typescript compilation
TNick Oct 21, 2023
33e71af
Editing interface tests
TNick Oct 21, 2023
76329b3
Added feature styles tests
TNick Oct 21, 2023
7b4f59b
Add test stubs. Format code in utils
TNick Oct 22, 2023
952b272
VectorLayerUtils tests
TNick Oct 22, 2023
180c509
Theme utils tests
TNick Oct 22, 2023
dd6a72d
image editor test
TNick Oct 22, 2023
62fdafe
A permalink test
TNick Oct 22, 2023
b735dfd
layers utils - partial
TNick Oct 24, 2023
0f76965
Merged master changes
TNick Oct 24, 2023
54a3b6c
use import instead of require in scripts
TNick Oct 24, 2023
c0fcbbd
Silence console warnings (arrays should be objects)
TNick Oct 24, 2023
84435f3
More layer tests. Changed name to id in implodeLayers
TNick Oct 26, 2023
8a49f28
Integrate upstream changes
TNick Oct 26, 2023
3f7f477
insertLayer and implodeLayers tests
TNick Oct 26, 2023
dae0f1e
Improve insertLayer docs
TNick Oct 26, 2023
623eb45
layerScaleInRange tests
TNick Oct 26, 2023
76e9afa
setGroupVisibilities tests
TNick Oct 26, 2023
186758b
WMS layers and friends
TNick Oct 27, 2023
90ed544
buildWMSLayerUrlParam tests
TNick Oct 27, 2023
a12510e
splitLayerUrlParam tests
TNick Oct 27, 2023
acb8dac
Introducing vscode settings (for ignored words in spell checker)
TNick Oct 28, 2023
d48798f
removeLayer tests
TNick Oct 28, 2023
ef9ea56
reorderLayer tests
TNick Oct 28, 2023
484d596
insertPermalinkLayers tests
TNick Oct 28, 2023
0eb674b
restoreLayerParams tests
TNick Oct 28, 2023
328bbb4
collectPrintParams - partial tests
TNick Oct 28, 2023
1474899
only compare visibility with false, as true and undefined means visible
TNick Oct 28, 2023
71e6cac
addExternalLayerPrintParams tests
TNick Oct 28, 2023
0837f28
completeExternalLayer tests
TNick Oct 28, 2023
3dae9f9
getLegendUrl tests
TNick Oct 28, 2023
cefb440
getSublayerNames tests
TNick Oct 28, 2023
2950d5b
mergeSubLayers tests
TNick Oct 28, 2023
fdde6ae
searchSubLayer tests
TNick Oct 28, 2023
325210d
searchLayer tests
TNick Oct 28, 2023
5b37956
getAttribution tests
TNick Oct 28, 2023
3ce872b
getTimeDimensionValues tests
TNick Oct 28, 2023
ca15471
restoreOrderedLayerParams tests
TNick Oct 28, 2023
7a52ecd
Fix tests for setGroupVisibilities
TNick Oct 28, 2023
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
Prev Previous commit
Next Next commit
Merged master changes
TNick committed Oct 24, 2023

Verified

This commit was signed with the committer’s verified signature. The key has expired.
TNick Nicu Tofan
commit 0f76965fb993cb38b73cdb9c65d8a81564397153
4 changes: 2 additions & 2 deletions doc/plugins.md
Original file line number Diff line number Diff line change
@@ -414,9 +414,9 @@ Allows exporting a selected portion of the map to a variety of formats.
| Property | Type | Description | Default value |
|----------|------|-------------|---------------|
| allowedFormats | `[string]` | Whitelist of allowed export format mimetypes. If empty, supported formats are listed. | `undefined` |
| allowedScales | `[number]` | List of scales at which to export the map. | `undefined` |
| allowedScales | `{[number], bool}` | List of scales at which to export the map. If empty, scale can be freely specified. If `false`, the map can only be exported at the current scale. | `undefined` |
| defaultFormat | `string` | Default export format mimetype. If empty, first available format is used. | `undefined` |
| defaultScaleFactor | `number` | The factor to apply to the map scale to determine the initial export map scale. | `0.5` |
| defaultScaleFactor | `number` | The factor to apply to the map scale to determine the initial export map scale (if `allowedScales` is not `false`). | `0.5` |
| dpis | `[number]` | List of dpis at which to export the map. If empty, the default server dpi is used. | `undefined` |
| exportExternalLayers | `bool` | Whether to include external layers in the image. Requires QGIS Server 3.x! | `true` |
| formatConfiguration | `{`<br />`  format: [{`<br />`  name: string,`<br />`  extraQuery: string,`<br />`  formatOptions: string,`<br />`  baseLayer: string,`<br />`}],`<br />`}` | Custom export configuration per format.<br /> If more than one configuration per format is provided, a selection combo will be displayed.<br /> `query` will be appended to the query string (replacing any existing parameters).<br /> `formatOptions` will be passed as FORMAT_OPTIONS.<br /> `baseLayer` will be appended to the LAYERS. | `undefined` |
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "qwc2",
"version": "2023.10.17-master",
"version": "2023.10.23-master",
"description": "QGIS Web Client 2 core",
"author": "Sourcepole AG",
"license": "BSD-2-Clause",
12 changes: 8 additions & 4 deletions plugins/FeatureSearch.jsx
Original file line number Diff line number Diff line change
@@ -13,12 +13,13 @@ import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import {v1 as uuidv1} from 'uuid';
import isEmpty from 'lodash.isempty';
import IdentifyViewer from '../components/IdentifyViewer';
import SideBar from '../components/SideBar';
import Spinner from '../components/Spinner';
import CoordinatesUtils from '../utils/CoordinatesUtils';
import IdentifyUtils from '../utils/IdentifyUtils';
import LocaleUtils from '../utils/LocaleUtils';
import "./style/FeatureSearch.css";
import IdentifyViewer from '../components/IdentifyViewer';

class FeatureSearch extends React.Component {
static propTypes = {
@@ -101,7 +102,10 @@ class FeatureSearch extends React.Component {
</tbody></table>
</fieldset>
<div className="feature-search-bar">
<button className="button" disabled={this.state.busy} type="submit">{LocaleUtils.tr("search.search")}</button>
<button className="button" disabled={this.state.busy} type="submit">
{this.state.busy ? (<Spinner />) : null}
{LocaleUtils.tr("search.search")}
</button>
</div>
</form>
);
@@ -128,7 +132,7 @@ class FeatureSearch extends React.Component {
{isEmpty(this.state.searchResults) ? (
<div className="feature-search-noresults">{LocaleUtils.tr("featuresearch.noresults")}</div>
) : (
<IdentifyViewer collapsible displayResultTree={false} identifyResults={this.state.searchResults} />
<IdentifyViewer collapsible displayResultTree={false} enableExport identifyResults={this.state.searchResults} />
)}
</div>
);
@@ -172,7 +176,7 @@ class FeatureSearch extends React.Component {
});
params.LAYERS = params.LAYERS.join(",");
params.FILTER = params.FILTER.join(";");
this.setState({busy: true});
this.setState({busy: true, searchResults: null});
axios.get(this.props.theme.featureInfoUrl, {params}).then(response => {
const results = IdentifyUtils.parseResponse(response.data, null, 'text/xml', null, this.props.map.projection);
this.setState({busy: false, searchResults: results});
15 changes: 0 additions & 15 deletions plugins/LocateButton.jsx
Original file line number Diff line number Diff line change
@@ -32,21 +32,6 @@ class LocateButton extends React.Component {
static defaultProps = {
position: 2
};
constructor(props) {
super(props);

if (!navigator.geolocation) {
props.changeLocateState("PERMISSION_DENIED");
} else {
navigator.geolocation.getCurrentPosition(() => {
// OK!
}, (err) => {
if (err.code === 1) {
props.changeLocateState("PERMISSION_DENIED");
}
});
}
}
onClick = () => {
if (this.props.locateState === "DISABLED") {
this.props.changeLocateState("ENABLED");
49 changes: 30 additions & 19 deletions plugins/MapExport.jsx
Original file line number Diff line number Diff line change
@@ -37,11 +37,11 @@ class MapExport extends React.Component {
static propTypes = {
/** Whitelist of allowed export format mimetypes. If empty, supported formats are listed. */
allowedFormats: PropTypes.arrayOf(PropTypes.string),
/** List of scales at which to export the map. */
allowedScales: PropTypes.arrayOf(PropTypes.number),
/** List of scales at which to export the map. If empty, scale can be freely specified. If `false`, the map can only be exported at the current scale. */
allowedScales: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.number), PropTypes.bool]),
/** Default export format mimetype. If empty, first available format is used. */
defaultFormat: PropTypes.string,
/** The factor to apply to the map scale to determine the initial export map scale. */
/** The factor to apply to the map scale to determine the initial export map scale (if `allowedScales` is not `false`). */
defaultScaleFactor: PropTypes.number,
/** List of dpis at which to export the map. If empty, the default server dpi is used. */
dpis: PropTypes.arrayOf(PropTypes.number),
@@ -107,11 +107,12 @@ class MapExport extends React.Component {
) {
if (this.state.pageSize !== null) {
this.setState((state) => {
const scale = this.getExportScale(state);
const center = this.props.map.center;
const mapCrs = this.props.map.projection;
const pageSize = this.props.pageSizes[state.pageSize];
const widthm = state.scale * pageSize.width / 1000;
const heightm = state.scale * pageSize.height / 1000;
const widthm = scale * pageSize.width / 1000;
const heightm = scale * pageSize.height / 1000;
const {width, height} = MapUtils.transformExtent(mapCrs, center, widthm, heightm);
let extent = [center[0] - 0.5 * width, center[1] - 0.5 * height, center[0] + 0.5 * width, center[1] + 0.5 * height];
extent = (CoordinatesUtils.getAxisOrder(mapCrs).substr(0, 2) === 'ne' && this.props.theme.version === '1.3.0') ?
@@ -156,7 +157,7 @@ class MapExport extends React.Component {
<select onChange={ev => this.setState({scale: ev.target.value})} role="input" value={this.state.scale}>
{this.props.allowedScales.map(scale => (<option key={scale} value={scale}>{scale}</option>))}
</select>);
} else {
} else if (this.props.allowedScales !== false) {
scaleChooser = (
<input min="1" onChange={ev => this.setState({scale: ev.target.value})} role="input" type="number" value={this.state.scale} />
);
@@ -167,7 +168,7 @@ class MapExport extends React.Component {

const mapScale = MapUtils.computeForZoom(this.props.map.scales, this.props.map.zoom);
let scaleFactor = 1;
if (this.state.pageSize === null) {
if (this.state.pageSize === null && this.props.allowedScales !== false) {
scaleFactor = mapScale / this.state.scale;
}
const exportParams = LayerUtils.collectPrintParams(this.props.layers, this.props.theme, mapScale, this.props.map.projection, exportExternalLayers);
@@ -213,15 +214,17 @@ class MapExport extends React.Component {
</td>
</tr>
) : null}
<tr>
<td>{LocaleUtils.tr("mapexport.scale")}</td>
<td>
<InputContainer>
<span role="prefix">1&nbsp;:&nbsp;</span>
{scaleChooser}
</InputContainer>
</td>
</tr>
{scaleChooser ? (
<tr>
<td>{LocaleUtils.tr("mapexport.scale")}</td>
<td>
<InputContainer>
<span role="prefix">1&nbsp;:&nbsp;</span>
{scaleChooser}
</InputContainer>
</td>
</tr>
) : null}
{this.props.dpis ? (
<tr>
<td>{LocaleUtils.tr("mapexport.resolution")}</td>
@@ -272,7 +275,7 @@ class MapExport extends React.Component {
};
renderFrame = () => {
if (this.state.pageSize !== null) {
const px2m = 1 / (this.state.dpi * 39.3701) * this.state.scale;
const px2m = 1 / (this.state.dpi * 39.3701) * this.getExportScale(this.state);
const frame = {
width: this.state.width * px2m,
height: this.state.height * px2m
@@ -332,6 +335,13 @@ class MapExport extends React.Component {
height: ''
});
};
getExportScale = (state) => {
if (this.props.allowedScales === false) {
return Math.round(MapUtils.computeForZoom(this.props.map.scales, this.props.map.zoom));
} else {
return state.scale;
}
};
bboxSelected = (bbox, crs, pixelsize) => {
const version = this.props.theme.version;
let extent = '';
@@ -368,7 +378,7 @@ class MapExport extends React.Component {

if (formatConfiguration) {
const keyCaseMap = Object.keys(params).reduce((res, key) => ({...res, [key.toLowerCase()]: key}), {});
(formatConfiguration.extraQuery || "").split(/[?&]/).forEach(entry => {
(formatConfiguration.extraQuery || "").split(/[?&]/).filter(Boolean).forEach(entry => {
const [key, value] = entry.split("=");
const caseKey = keyCaseMap[key.toLowerCase()] || key;
params[caseKey] = (value ?? "");
@@ -392,7 +402,8 @@ class MapExport extends React.Component {
axios.post(this.props.theme.url, data, config).then(response => {
this.setState({exporting: false});
const contentType = response.headers["content-type"];
FileSaver.saveAs(new Blob([response.data], {type: contentType}), this.props.theme.name + '.pdf');
const ext = this.state.selectedFormat.split(";")[0].split("/").pop();
FileSaver.saveAs(new Blob([response.data], {type: contentType}), this.props.theme.name + '.' + ext);
}).catch(e => {
this.setState({exporting: false});
if (e.response) {
13 changes: 9 additions & 4 deletions plugins/Redlining.jsx
Original file line number Diff line number Diff line change
@@ -67,8 +67,13 @@ class Redlining extends React.Component {
if (prevProps.redlining.geomType !== this.props.redlining.geomType && this.props.redlining.geomType === 'Text' && !this.state.selectText) {
this.setState({selectText: true});
}
if (!this.props.layers.find(layer => layer.id === this.props.redlining.layer) && this.props.redlining.layer !== 'redlining') {
this.props.changeRedliningState({layer: 'redlining', layerTitle: 'Redlining'});
if (!this.props.layers.find(layer => layer.id === this.props.redlining.layer)) {
const vectorLayers = this.props.layers.filter(layer => layer.type === "vector" && layer.role === LayerRole.USERLAYER && !layer.readonly);
if (vectorLayers.length >= 1) {
this.props.changeRedliningState({layer: vectorLayers[0].id, layerTitle: vectorLayers[0].title});
} else if (this.props.redlining.layer !== 'redlining') {
this.props.changeRedliningState({layer: 'redlining', layerTitle: 'Redlining'});
}
}
}
componentWillUnmount() {
@@ -139,8 +144,8 @@ class Redlining extends React.Component {
editButtons.push(plugin.cfg);
}
let vectorLayers = this.props.layers.filter(layer => layer.type === "vector" && layer.role === LayerRole.USERLAYER && !layer.readonly);
// Ensure list always contains "Redlining" layer
if (!vectorLayers.find(layer => layer.id === 'redlining')) {
// Ensure list always contains at least a "Redlining" layer
if (vectorLayers.length === 0) {
vectorLayers = [{id: 'redlining', title: 'Redlining'}, ...vectorLayers];
}

7 changes: 6 additions & 1 deletion plugins/map/LocateSupport.jsx
Original file line number Diff line number Diff line change
@@ -85,7 +85,12 @@ class LocateSupport extends React.Component {
};
onLocationError = (err) => {
this.props.onLocateError(err.message);
this.props.changeLocateState("DISABLED");
// User denied geolocation prompt
if (err.code === 1) {
this.props.changeLocateState("PERMISSION_DENIED");
} else {
this.props.changeLocateState("DISABLED");
}
};
render() {
return null;
15 changes: 10 additions & 5 deletions plugins/style/Buttons.css
Original file line number Diff line number Diff line change
@@ -16,11 +16,6 @@ button.map-button {
cursor: pointer;
}

button.map-button.locate-button-DISABLED {
opacity: 0.7;
cursor: default;
}

button.map-button:hover {
background-color: var(--map-button-hover-bg-color);
color: var(--map-button-hover-text-color);
@@ -31,6 +26,16 @@ button.map-button-active {
color: var(--map-button-active-text-color);
}

button.map-button.locate-button-PERMISSION_DENIED {
opacity: 0.7;
cursor: default;
}

button.map-button.locate-button-PERMISSION_DENIED:hover {
background-color: var(--map-button-bg-color);
color: var(--map-button-text-color);
}

button.map-button.locate-button-LOCATING,
button.map-button.locate-button-ENABLED {
background-color: var(--map-button-text-color);
6 changes: 6 additions & 0 deletions plugins/style/FeatureSearch.css
Original file line number Diff line number Diff line change
@@ -47,6 +47,12 @@ div.feature-search-bar > button {
width: 100%;
}

div.feature-search-bar > button > div.Spinner {
width: 2em;
height: 2em;
margin-right: 1em;
}

div.feature-search-results {
flex: 1 1 auto;
overflow-y: auto;
21 changes: 18 additions & 3 deletions utils/LayerUtils.js
Original file line number Diff line number Diff line change
@@ -1089,9 +1089,24 @@ const LayerUtils = {
}
}
if (printBgLayerName) {
params.LAYERS.push(printBgLayerName);
params.OPACITIES.push("255");
params.COLORS.push("");
let match = null;
if (
(match = printBgLayerName.match(/^(\w+):(.*)#([^#]+)$/)) &&
match[1] === "wms"
) {
const layer = {
type: 'wms',
params: {LAYERS: match[3], OPACITIES: '255'},
url: match[2]
};
LayerUtils.addExternalLayerPrintParams(
layer, params, printCrs, counterRef
);
} else {
params.LAYERS.push(printBgLayerName);
params.OPACITIES.push("255");
params.COLORS.push("");
}
}
} else if (printExternalLayers) {
// Inject client-side wms as external layer for print
9 changes: 9 additions & 0 deletions utils/VectorLayerUtils.js
Original file line number Diff line number Diff line change
@@ -87,6 +87,15 @@ const VectorLayerUtils = {
let geometry = VectorLayerUtils.reprojectGeometry(
feature.geometry, feature.crs || printCrs, printCrs
);
// Filter degenerate geometries coordinates
if (feature.geometry.type === "LineString") {
const filteredCoordinates = geometry.coordinates.filter((item, pos, arr) => {
return pos === 0 || item[0] !== arr[pos - 1][0] || item[1] !== arr[pos - 1][1];
});
if (filteredCoordinates.length < 2) {
continue;
}
}
if (
feature.geometry.type === "LineString" &&
!isEmpty(properties.segment_labels)