Skip to content

Commit bd4c47e

Browse files
committed
Add draft GeoStyler page
1 parent d58288d commit bd4c47e

12 files changed

Lines changed: 318 additions & 45 deletions

File tree

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Vector Styling in OpenLayers using GeoStyler
2+
3+
!!! warning
4+
5+
This page is currently in a draft form. It may also require features
6+
only available in the `main` MapServer branch, and not yet released in a stable version.
7+
8+
## Overview
9+
10+
[GeoStyler](https://geostyler.org/) is *an open-source React library that enables developers to build flexible and powerful graphical user interfaces for map-styling in the browser*.
11+
12+
It has many different parsers that allow conversion of one style format to another. In this example, we will use the [SLD parser](https://geostyler.org/docs/usage/parsers/sld/)
13+
to convert SLD created by MapServer styles into GeoStyler JSON format, which can then be used to style vector layers in OpenLayers.
14+
15+
16+
<div class="map" style="height: 600px; border: 1px solid black;">
17+
<iframe src="https://mapserver.github.io/getting-started-with-mapserver-demo/geostyler.html"></iframe>
18+
</div>
19+
20+
21+
## Mapfile Notes
22+
23+
### Symbols
24+
25+
Symbols are added to the Mapfile using `INCLUDE` directives, which allows them to be loaded into the
26+
demo web application. The symbol `NAME` matches the `LAYER` name.
27+
28+
### Symbol Resolution
29+
30+
[RESOLUTION](https://mapserver.org/mapfile/map.html#mapfile-map-resolution) and [DEFRESOLUTION](https://mapserver.org/mapfile/map.html#mapfile-map-defresolution)
31+
are used to control the scaling of symbols in MapServer.
32+
33+
When a WMS source has `serverType: 'mapserver', OpenLayers requests HiDPI images by adding
34+
`MAP_RESOLUTION` = 90 × device pixel ratio to the request (when the ratio isn't 1).
35+
The 90 value is [hard-coded in OpenLayers](https://github.com/openlayers/openlayers/blob/69e07d8ad933c3081cc10b8bb4a64023917661aa/src/ol/source/wms.js#L99)
36+
and based on the OGC standard rendering pixel size of 0.28 mm (25.4 mm per inch ÷ 0.28 mm per pixel = 90.71 pixels per inch).
37+
38+
The device pixel ratio is a property of the browser, and can be different on different devices.
39+
To find your browser pixel device ratio, open the developer tools and check the `window.devicePixelRatio` value.
40+
41+
For example, if this is `1.5`, then the `MAP_RESOLUTION` parameter sent by OpenLayers to MapServer as part of the WMS request
42+
will be `90 * 1.5 = 135`. The image is requested at 1.5 times the size of the map. When this parameter is sent
43+
by a client, it overrides the `RESOLUTION` value set in the Mapfile for that request.
44+
45+
MapServer scales symbol sizes by `RESOLUTION / DEFRESOLUTION`. Setting both to 90 keeps symbols
46+
at the same visual size on any screen, matching the SLD sizes used for client-side styling.
47+
When `MAP_RESOLUTION` is 135, symbols are scaled by 1.5 in an image that the browser displays 1.5 times smaller.
48+
49+
With MapServer's defaults of 72, symbols would instead be scaled by `135 / 72 = 1.875`
50+
in the image, so they appear 1.25 times too large on HiDPI screens.
51+
52+
53+
```scala
54+
RESOLUTION 90 # if no MAP_RESOLUTION is provided in a request, MapServer uses this value
55+
DEFRESOLUTION 90 # set to match OpenLayers default. MapServer default is 72
56+
```
57+
58+
## Applying Styles with GeoStyler
59+
60+
61+
## Code
62+
63+
!!! example
64+
65+
- Inbuilt OpenLayers request: <http://localhost:7000/GEOSTYLER/?template=openlayers&mode=browse&layers=all>
66+
- OpenLayers and SLD viewer: <http://localhost:7001/geostyler.html>
67+
- Direct `GetStyles` request: <http://localhost:7000/?map=/etc/mapserver/geostyler.map&REQUEST=GetStyles&SERVICE=WMS&VERSION=1.3.0&LAYERS=notched-rect>
68+
69+
??? JavaScript "geostyler.js"
70+
71+
``` js
72+
--8<-- "geostyler.js"
73+
```
74+
75+
??? Mapfile "geostyler.map"
76+
77+
``` scala
78+
--8<-- "geostyler.map"
79+
```
80+
81+
82+
## Exercises
83+
84+
1. Play around with the `RESOLUTION` and `DEFRESOLUTION` values in the Mapfile and see how it affects the symbol sizes.

‎workshop/content/zensical.toml‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ nav = [
5353
{ "Clusters" = "advanced/clusters.md" },
5454
{ "SLD" = "advanced/sld.md" },
5555
{ "STAC" = "advanced/stac.md" },
56-
{ "Non-EPSG CRSs" = "advanced/other-projections.md" }
56+
{ "Non-EPSG CRSs" = "advanced/other-projections.md" },
57+
{ "GeoStyler" = "advanced/geostyler.md" }
5758
]},
5859
{ "Summary" = "summary.md" },
5960
{ "Credits and Thanks" = "credits.md" }

‎workshop/exercises/app/css/geostyler.css‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,30 @@ html, body {
129129
.code .cm-content {
130130
padding-bottom: 40px;
131131
}
132+
133+
/* `display: flex` on .code would otherwise override the hidden attribute */
134+
.code[hidden] {
135+
display: none;
136+
}
137+
138+
/* Tabs in the bottom-right panel title bar */
139+
.tabs {
140+
display: flex;
141+
gap: 4px;
142+
padding-bottom: 0;
143+
}
144+
145+
.tab {
146+
font: inherit;
147+
color: #777;
148+
background: none;
149+
border: none;
150+
border-bottom: 2px solid transparent;
151+
padding: 0 6px 3px;
152+
cursor: pointer;
153+
}
154+
155+
.tab.active {
156+
color: #444;
157+
border-bottom-color: #444;
158+
}
266 KB
Binary file not shown.

‎workshop/exercises/app/geostyler.html‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,12 @@
3838
</div>
3939
</div>
4040
<div class="panel">
41-
<div class="panel-title">SLD (applied in OpenLayers)</div>
41+
<div class="panel-title tabs" role="tablist">
42+
<button class="tab active" role="tab" data-tab="sld-text">SLD</button>
43+
<button class="tab" role="tab" data-tab="geostyler-text">GeoStyler style (used by OpenLayers)</button>
44+
</div>
4245
<div id="sld-text" class="code"></div>
46+
<div id="geostyler-text" class="code" hidden></div>
4347
</div>
4448
</section>
4549
</main>

‎workshop/exercises/app/js/geostyler.js‎

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import xmlFormat from 'xml-formatter';
1414
import { EditorView, basicSetup } from 'codemirror';
1515
import { EditorState } from '@codemirror/state';
1616
import { xml } from '@codemirror/lang-xml';
17+
import { json } from '@codemirror/lang-json';
1718

1819
const mapserverUrl = import.meta.env.VITE_MAPSERVER_BASE_URL;
1920
const mapfilesPath = import.meta.env.VITE_MAPFILES_PATH;
@@ -33,6 +34,7 @@ const demoLayers = [
3334
{ id: 'notched-rect', type: 'point' },
3435
{ id: 'square-spaced', type: 'polygon' },
3536
{ id: 'wave', type: 'polygon' },
37+
{ id: 'fills', type: 'polygon' },
3638
];
3739

3840
const sldParser = new SLDParser();
@@ -72,6 +74,7 @@ function setText(view, text) {
7274
const classView = createViewer('class-text', mapfileLanguage);
7375
const symView = createViewer('sym-text', mapfileLanguage);
7476
const sldView = createViewer('sld-text', xml());
77+
const geostylerView = createViewer('geostyler-text', json());
7578

7679
function showSld(text) {
7780
let formatted = text;
@@ -124,20 +127,14 @@ async function fetchSld(layerConfig) {
124127

125128
/**
126129
* Convert an SLD to an OpenLayers style with GeoStyler, and apply it.
130+
* Returns the GeoStyler style the OpenLayers style was created from.
127131
*/
128132
async function applySld(sldXmlString, vectorLayer) {
129133
const gs = await sldParser.readStyle(sldXmlString);
130134
if (gs.errors) {
131135
throw new Error(`Errors parsing the SLD: ${gs.errors}`);
132136
}
133137

134-
// MapServer applies the first matching CLASS, so a final CLASS without an
135-
// EXPRESSION means "everything else". In SLD a rule without a filter
136-
// matches every feature, so mark such rules as else rules.
137-
const rules = gs.output.rules;
138-
if (rules.some((r) => r.filter)) {
139-
rules.filter((r) => !r.filter).forEach((r) => { r.elseRule = true; });
140-
}
141138
console.log(`GeoStyler style for layer ${vectorLayer.get('name')}:`, gs.output);
142139

143140
const ol = await olParser.writeStyle(gs.output);
@@ -148,6 +145,7 @@ async function applySld(sldXmlString, vectorLayer) {
148145
console.log(`OpenLayers style for layer ${vectorLayer.get('name')}:`, ol.output);
149146

150147
vectorLayer.setStyle(ol.output);
148+
return gs.output;
151149
}
152150

153151
function createVectorLayer(layerConfig) {
@@ -206,19 +204,20 @@ function main() {
206204
const classFile = `${layerConfig.id}.class`;
207205
const symFile = `${layerConfig.id}.sym`;
208206

209-
const [classText, symText, sld] = await Promise.all([
207+
const [classText, symText, { sld, geostylerStyle }] = await Promise.all([
210208
fetchText(`${includesUrl}${classFile}`)
211209
.catch((error) => `# Could not load ${classFile}: ${error.message}`),
212210
fetchOptionalText(`${includesUrl}${symFile}`)
213211
.catch(() => null),
214212
fetchSld(layerConfig)
215213
.then(async (sldXmlString) => {
216-
await applySld(sldXmlString, vectorLayer);
217-
return sldXmlString;
214+
const style = await applySld(sldXmlString, vectorLayer);
215+
return { sld: sldXmlString, geostylerStyle: style };
218216
})
219217
.catch((error) => {
220218
console.error(`Failed to style layer ${layerConfig.id}`, error);
221-
return `Could not load or apply the SLD: ${error.message}`;
219+
const message = `Could not load or apply the SLD: ${error.message}`;
220+
return { sld: message, geostylerStyle: null };
222221
}),
223222
]);
224223

@@ -235,8 +234,24 @@ function main() {
235234
setText(symView, symText ?? '');
236235

237236
showSld(sld);
237+
setText(geostylerView, geostylerStyle
238+
? JSON.stringify(geostylerStyle, null, 2)
239+
: '// No GeoStyler style: see the SLD tab for the error');
238240

239241
mapRight.getLayers().clear();
242+
243+
// Tabs in the bottom-right panel: show one viewer at a time
244+
const tabs = document.querySelectorAll('.tab');
245+
tabs.forEach((tab) => {
246+
tab.addEventListener('click', () => {
247+
tabs.forEach((t) => {
248+
const active = t === tab;
249+
t.classList.toggle('active', active);
250+
document.getElementById(t.dataset.tab).hidden = !active;
251+
});
252+
});
253+
});
254+
240255
mapRight.addLayer(baseLayer());
241256
mapRight.addLayer(vectorLayer);
242257
}

0 commit comments

Comments
 (0)