diff --git a/workshop/exercises/app/css/geostyler.css b/workshop/exercises/app/css/geostyler.css
new file mode 100644
index 0000000..83200ad
--- /dev/null
+++ b/workshop/exercises/app/css/geostyler.css
@@ -0,0 +1,131 @@
+@import "node_modules/ol/ol.css";
+
+html, body {
+ margin: 0;
+ padding: 0;
+ height: 100%;
+ font-family: sans-serif;
+}
+
+/* 2 x 2 layout: maps on top, source text below */
+#container {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+}
+
+#maps,
+#sources {
+ flex: 1;
+ display: flex;
+ min-height: 0; /* lets the halves shrink so the code panels can scroll */
+}
+
+#maps {
+ border-bottom: 2px solid #ccc;
+}
+
+.panel {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ min-height: 0;
+}
+
+ .panel + .panel {
+ border-left: 2px solid #ccc;
+ }
+
+.panel-title {
+ padding: 4px 10px;
+ font-size: 12px;
+ font-weight: bold;
+ color: #444;
+ background: #f3f3f3;
+ border-bottom: 1px solid #ddd;
+}
+
+.file-name {
+ font-weight: normal;
+ color: #777;
+ font-family: Consolas, "Courier New", monospace;
+}
+
+/* Maps */
+.map {
+ flex: 1;
+ min-height: 0;
+}
+
+/* Greyscale basemap, so the styled layers stand out */
+.bw {
+ filter: grayscale(100%);
+}
+
+/* The layer selector sits in the top-right corner of the left map */
+#panel-left {
+ position: relative;
+}
+
+#toolbar {
+ position: absolute;
+ top: 32px; /* just below the panel title bar */
+ right: 8px;
+ z-index: 1000;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ background: white;
+ padding: 6px 10px;
+ border-radius: 6px;
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
+ font-size: 13px;
+}
+
+#layer-select {
+ font-size: 13px;
+ padding: 3px 6px;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+}
+
+/* Bottom-left panel: Classes takes any remaining space, Symbols only what
+ it needs. If both don't fit, they shrink in proportion to their content. */
+.subpanel {
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+}
+
+ .subpanel[hidden] {
+ display: none;
+ }
+
+#class-panel {
+ flex: 1 1 auto;
+}
+
+#sym-panel {
+ flex: 0 1 auto;
+ border-top: 2px solid #ccc;
+}
+
+/* Read-only CodeMirror viewers */
+.code {
+ flex: 1 1 auto;
+ min-height: 0;
+ display: flex;
+ overflow: hidden;
+}
+
+ .code .cm-editor {
+ flex: 1;
+ min-height: 0;
+ font-size: 12px;
+ }
+
+ /* Keep the last line visible above a horizontal scrollbar */
+ .code .cm-content {
+ padding-bottom: 40px;
+ }
diff --git a/workshop/exercises/app/geostyler.html b/workshop/exercises/app/geostyler.html
new file mode 100644
index 0000000..f3cce2e
--- /dev/null
+++ b/workshop/exercises/app/geostyler.html
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+ OpenLayers and GeoStyler Demo
+
+
+
+
+
+
+
MapServer WMS
+
+
+
+
+
+
+
+
OpenLayers + GeoStyler (SLD)
+
+
+
+
+
+
+
+
+
SLD (applied in OpenLayers)
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/workshop/exercises/app/index.html b/workshop/exercises/app/index.html
index 4c47800..2366a88 100644
--- a/workshop/exercises/app/index.html
+++ b/workshop/exercises/app/index.html
@@ -44,6 +44,7 @@ Advanced
SLD - Styled Layer Descriptor
STAC
Working with non-EPSG Coordinate Reference Systems (CRS)
+ Working with GeoStyler
Miscellaneous
diff --git a/workshop/exercises/app/js/geostyler.js b/workshop/exercises/app/js/geostyler.js
new file mode 100644
index 0000000..66d1b6e
--- /dev/null
+++ b/workshop/exercises/app/js/geostyler.js
@@ -0,0 +1,260 @@
+import '../css/geostyler.css';
+import { Map, View } from 'ol';
+import TileLayer from 'ol/layer/Tile';
+import ImageLayer from 'ol/layer/Image';
+import VectorLayer from 'ol/layer/Vector';
+import OSM from 'ol/source/OSM';
+import ImageWMS from 'ol/source/ImageWMS';
+import VectorSource from 'ol/source/Vector';
+import GeoJSON from 'ol/format/GeoJSON';
+import { bbox as bboxStrategy } from 'ol/loadingstrategy';
+import SLDParser from 'geostyler-sld-parser';
+import OpenLayersParser from 'geostyler-openlayers-parser';
+import xmlFormat from 'xml-formatter';
+import { EditorView, basicSetup } from 'codemirror';
+import { EditorState } from '@codemirror/state';
+import { xml } from '@codemirror/lang-xml';
+
+const mapserverUrl = import.meta.env.VITE_MAPSERVER_BASE_URL;
+const mapfilesPath = import.meta.env.VITE_MAPFILES_PATH;
+
+const serverUrl = `${mapserverUrl}${mapfilesPath}geostyler.map&`;
+const includesUrl = `${import.meta.env.BASE_URL}includes/`;
+
+/**
+ * Layers available in the demo.
+ *
+ * - id: the MapServer LAYER name.
+ * - type: the geometry type, shown in the layer selector
+ * - title (optional): label in the layer selector
+ */
+const demoLayers = [
+ { id: 'arrow-wide', type: 'line' },
+ { id: 'notched-rect', type: 'point' },
+ { id: 'square-spaced', type: 'polygon' },
+ { id: 'wave', type: 'polygon' },
+];
+
+const sldParser = new SLDParser();
+const olParser = new OpenLayersParser();
+
+const baseLayer = () => new TileLayer({ source: new OSM(), opacity: 0.5, className: 'bw' });
+
+const sharedView = new View({
+ center: [2975862, 8046369],
+ zoom: 14,
+});
+
+// TODO add Mapfile syntax
+const mapfileLanguage = [];
+
+/**
+ * Read-only code viewer with line numbers, folding and search (Ctrl+F)
+ */
+function createViewer(parentId, language) {
+ return new EditorView({
+ parent: document.getElementById(parentId),
+ state: EditorState.create({
+ extensions: [
+ basicSetup,
+ language,
+ EditorState.readOnly.of(true),
+ EditorView.editable.of(false),
+ ],
+ }),
+ });
+}
+
+function setText(view, text) {
+ view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: text } });
+}
+
+const classView = createViewer('class-text', mapfileLanguage);
+const symView = createViewer('sym-text', mapfileLanguage);
+const sldView = createViewer('sld-text', xml());
+
+function showSld(text) {
+ let formatted = text;
+ try {
+ formatted = xmlFormat(text, { indentation: ' ', collapseContent: true });
+ } catch {
+ // not XML (e.g. an error message): show as is
+ }
+ setText(sldView, formatted);
+}
+
+async function fetchText(url) {
+ const response = await fetch(url);
+ if (!response.ok) {
+ throw new Error(`${response.status} ${response.statusText}`);
+ }
+ return response.text();
+}
+
+/**
+ * Fetch a file that may not exist. Returns null if it is missing.
+ * The Vite dev server answers unknown paths with index.html (status 200),
+ * so an HTML response is also treated as missing.
+ */
+async function fetchOptionalText(url) {
+ const response = await fetch(url);
+ const contentType = response.headers.get('Content-Type') || '';
+ if (!response.ok || contentType.includes('text/html')) {
+ return null;
+ }
+ return response.text();
+}
+
+/**
+ * Fetch the SLD for a layer with a GetStyles request, and return it as a string.
+ */
+async function fetchSld(layerConfig) {
+ const sldUrl = `${serverUrl}REQUEST=GetStyles&SERVICE=WMS&VERSION=1.3.0&LAYERS=${layerConfig.id}`;
+ const response = await fetch(sldUrl);
+ if (!response.ok) {
+ throw new Error(`Failed to fetch SLD: ${response.status} ${response.statusText}`);
+ }
+ const contentType = response.headers.get('Content-Type') || '';
+ if (!contentType.includes('xml')) {
+ throw new Error(`Unexpected Content-Type: ${contentType}. Expected XML.`);
+ }
+
+ return response.text();
+}
+
+/**
+ * Convert an SLD to an OpenLayers style with GeoStyler, and apply it.
+ */
+async function applySld(sldXmlString, vectorLayer) {
+ const gs = await sldParser.readStyle(sldXmlString);
+ if (gs.errors) {
+ throw new Error(`Errors parsing the SLD: ${gs.errors}`);
+ }
+
+ // MapServer applies the first matching CLASS, so a final CLASS without an
+ // EXPRESSION means "everything else". In SLD a rule without a filter
+ // matches every feature, so mark such rules as else rules.
+ const rules = gs.output.rules;
+ if (rules.some((r) => r.filter)) {
+ rules.filter((r) => !r.filter).forEach((r) => { r.elseRule = true; });
+ }
+ console.log(`GeoStyler style for layer ${vectorLayer.get('name')}:`, gs.output);
+
+ const ol = await olParser.writeStyle(gs.output);
+
+ if (ol.errors) {
+ throw new Error(`Errors writing the OpenLayers style: ${ol.errors}`);
+ }
+ console.log(`OpenLayers style for layer ${vectorLayer.get('name')}:`, ol.output);
+
+ vectorLayer.setStyle(ol.output);
+}
+
+function createVectorLayer(layerConfig) {
+ const source = new VectorSource({
+ format: new GeoJSON({
+ dataProjection: 'EPSG:3857',
+ featureProjection: 'EPSG:3857',
+ }),
+ url: (extent, resolution, projection) => {
+ const epsg = projection.getCode();
+ return `${serverUrl}REQUEST=GetFeature&SERVICE=WFS&VERSION=2.0.0&OUTPUTFORMAT=geojson&TYPENAMES=${layerConfig.id}&srsName=${epsg}&BBOX=${extent.join(',')},${epsg}`;
+ },
+ strategy: bboxStrategy,
+ });
+ return new VectorLayer({ source, name: layerConfig.id });
+}
+
+function main() {
+ const classFileName = document.getElementById('class-file-name');
+ const symFileName = document.getElementById('sym-file-name');
+ const symPanel = document.getElementById('sym-panel');
+
+ const wmsSource = new ImageWMS({
+ url: serverUrl,
+ params: {
+ VERSION: '1.3.0',
+ FORMAT: 'image/png',
+ TRANSPARENT: true,
+ },
+ serverType: 'mapserver',
+ });
+
+ new Map({
+ target: 'map-left',
+ layers: [baseLayer(), new ImageLayer({ source: wmsSource })],
+ view: sharedView,
+ });
+
+ const mapRight = new Map({
+ target: 'map-right',
+ layers: [],
+ view: sharedView,
+ });
+
+ let switchToken = 0;
+ async function switchLayer(layerConfig) {
+
+ const token = ++switchToken;
+
+ // Left map: MapServer renders the layer itself
+ wmsSource.updateParams({ LAYERS: layerConfig.id });
+
+ // Right map: WFS features styled in OpenLayers from the SLD
+ const vectorLayer = createVectorLayer(layerConfig);
+
+ const classFile = `${layerConfig.id}.class`;
+ const symFile = `${layerConfig.id}.sym`;
+
+ const [classText, symText, sld] = await Promise.all([
+ fetchText(`${includesUrl}${classFile}`)
+ .catch((error) => `# Could not load ${classFile}: ${error.message}`),
+ fetchOptionalText(`${includesUrl}${symFile}`)
+ .catch(() => null),
+ fetchSld(layerConfig)
+ .then(async (sldXmlString) => {
+ await applySld(sldXmlString, vectorLayer);
+ return sldXmlString;
+ })
+ .catch((error) => {
+ console.error(`Failed to style layer ${layerConfig.id}`, error);
+ return `Could not load or apply the SLD: ${error.message}`;
+ }),
+ ]);
+
+ if (token !== switchToken) {
+ return; // a newer switch happened, discard this one
+ }
+
+ classFileName.textContent = classFile;
+ setText(classView, classText);
+
+ // The symbols file is optional: hide its panel when there is none
+ symPanel.hidden = symText === null;
+ symFileName.textContent = symText === null ? '' : symFile;
+ setText(symView, symText ?? '');
+
+ showSld(sld);
+
+ mapRight.getLayers().clear();
+ mapRight.addLayer(baseLayer());
+ mapRight.addLayer(vectorLayer);
+ }
+
+ // Populate the layer selector
+ const select = document.getElementById('layer-select');
+ demoLayers.forEach((layer, index) => {
+ const option = document.createElement('option');
+ option.value = String(index);
+ option.textContent = layer.title ?? `${layer.id} (${layer.type})`;
+ select.appendChild(option);
+ });
+
+ select.addEventListener('change', (e) => {
+ switchLayer(demoLayers[Number(e.target.value)]);
+ });
+
+ switchLayer(demoLayers[0]);
+}
+
+main();
\ No newline at end of file
diff --git a/workshop/exercises/app/package-lock.json b/workshop/exercises/app/package-lock.json
index 3599056..629328a 100644
--- a/workshop/exercises/app/package-lock.json
+++ b/workshop/exercises/app/package-lock.json
@@ -8,15 +8,117 @@
"name": "getting-started-with-mapserver",
"version": "1.3.0",
"dependencies": {
+ "@codemirror/lang-xml": "^6.1.0",
+ "@codemirror/language": "^6.11.0",
+ "@codemirror/state": "^6.5.0",
"bootstrap": "^5.3.7",
+ "codemirror": "^6.0.1",
+ "geostyler-openlayers-parser": "file:vendor/geostyler-openlayers-parser-5.7.1.tgz",
+ "geostyler-sld-parser": "^9.0.3",
"ol": "^10.8.0",
- "proj4": "^2.20.3"
+ "proj4": "^2.20.3",
+ "xml-formatter": "^3.6.3"
},
"devDependencies": {
"rollup-plugin-copy": "^3.5.0",
"vite": "^7.3.1"
}
},
+ "node_modules/@codemirror/autocomplete": {
+ "version": "6.20.3",
+ "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.3.tgz",
+ "integrity": "sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.17.0",
+ "@lezer/common": "^1.0.0"
+ }
+ },
+ "node_modules/@codemirror/commands": {
+ "version": "6.11.1",
+ "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.11.1.tgz",
+ "integrity": "sha512-O/4hG3SC1YwcmQ0d2UVNDs+AsaNWd1iHVxbTeEBuqH+6bExAiPK3iS/BvpY6rZGURALv4ZD3sIgcCmRvw3ehBg==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/state": "^6.7.0",
+ "@codemirror/view": "^6.27.0",
+ "@lezer/common": "^1.1.0"
+ }
+ },
+ "node_modules/@codemirror/lang-xml": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.1.0.tgz",
+ "integrity": "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/autocomplete": "^6.0.0",
+ "@codemirror/language": "^6.4.0",
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.0.0",
+ "@lezer/common": "^1.0.0",
+ "@lezer/xml": "^1.0.0"
+ }
+ },
+ "node_modules/@codemirror/language": {
+ "version": "6.12.4",
+ "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.4.tgz",
+ "integrity": "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.23.0",
+ "@lezer/common": "^1.5.0",
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0",
+ "style-mod": "^4.0.0"
+ }
+ },
+ "node_modules/@codemirror/lint": {
+ "version": "6.9.7",
+ "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.7.tgz",
+ "integrity": "sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.42.0",
+ "crelt": "^1.0.5"
+ }
+ },
+ "node_modules/@codemirror/search": {
+ "version": "6.7.2",
+ "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.2.tgz",
+ "integrity": "sha512-gUYkYhT2+n/+VGZ+8EzE5WFkYZUZYm1VOKDudIsNqh42uRVQJ0a6Yss9sdKT3MeOYfuL1N6AZA57oza0Oyr0LA==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.37.0",
+ "crelt": "^1.0.5"
+ }
+ },
+ "node_modules/@codemirror/state": {
+ "version": "6.7.6",
+ "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.7.6.tgz",
+ "integrity": "sha512-kAz+AncRtKuIknedxT1bq4XwXv4UowhbkHU1myPrtVb/jZtImWuV5BXzv5vK6i3kYACsdiZiQKFQQ5Mq7elW8w==",
+ "license": "MIT",
+ "dependencies": {
+ "@marijn/find-cluster-break": "^1.0.0"
+ }
+ },
+ "node_modules/@codemirror/view": {
+ "version": "6.43.13",
+ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.13.tgz",
+ "integrity": "sha512-sihaFrUzAsYBQsL9J2t69y8nfMQGwcYmggAZsk+kjPbjYZMyuf2hU8tUNTZ+P+isb6XRr8JE22TZlJxBoVdH1A==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/state": "^6.7.0",
+ "crelt": "^1.0.6",
+ "style-mod": "^4.1.0",
+ "w3c-keyname": "^2.2.4"
+ }
+ },
"node_modules/@esbuild/aix-ppc64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
@@ -459,6 +561,59 @@
"node": ">=18"
}
},
+ "node_modules/@lezer/common": {
+ "version": "1.5.3",
+ "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.3.tgz",
+ "integrity": "sha512-H0iErY4e43LpXbYDyBci5W4v/RwTgGV3YzYOlJPiGZ8RY8w51kVE+xLn1tg3Z6UmSfyETvtsUK3r7YsgDQEI7Q==",
+ "license": "MIT"
+ },
+ "node_modules/@lezer/highlight": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.5.tgz",
+ "integrity": "sha512-O1GMVKgtf5YspFaRzpmqkVgMtIk2HG9uMAbH6wdAIdNh3TVzCsch/+cRi+6l2//UGDKe6NB9BW3cvuR5ezMQ0w==",
+ "license": "MIT",
+ "dependencies": {
+ "@lezer/common": "^1.3.0"
+ }
+ },
+ "node_modules/@lezer/lr": {
+ "version": "1.4.10",
+ "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz",
+ "integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==",
+ "license": "MIT",
+ "dependencies": {
+ "@lezer/common": "^1.0.0"
+ }
+ },
+ "node_modules/@lezer/xml": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.6.tgz",
+ "integrity": "sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==",
+ "license": "MIT",
+ "dependencies": {
+ "@lezer/common": "^1.2.0",
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0"
+ }
+ },
+ "node_modules/@marijn/find-cluster-break": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.4.tgz",
+ "integrity": "sha512-Wy0V7+SGUjnF9/TkiM1hKVDPj7jKXduPNboMVtHTA8dySMURWqfg/JZ9E2Sq8JgSJmkl7k7Qe9FLeMSrSraWmQ==",
+ "license": "MIT"
+ },
+ "node_modules/@nodable/entities": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-3.1.0.tgz",
+ "integrity": "sha512-LsS/DjHr+uDM647Gru/cA8+J3a3HfhttwCKLyuoyN7yXTFCxKBtSgMQBvrW9yNPa2/zDRUNuGPaH5QUFoa4arQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nodable"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -855,6 +1010,18 @@
"unzipit": "1.4.3"
}
},
+ "node_modules/anynum": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz",
+ "integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/array-union": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
@@ -915,6 +1082,21 @@
"node": ">=8"
}
},
+ "node_modules/codemirror": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
+ "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/autocomplete": "^6.0.0",
+ "@codemirror/commands": "^6.0.0",
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/lint": "^6.0.0",
+ "@codemirror/search": "^6.0.0",
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.0.0"
+ }
+ },
"node_modules/colorette": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz",
@@ -929,6 +1111,18 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/crelt": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz",
+ "integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==",
+ "license": "MIT"
+ },
+ "node_modules/css-font-parser": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/css-font-parser/-/css-font-parser-2.0.1.tgz",
+ "integrity": "sha512-C4aQOpCmQL/Arl68chQatNh7/Nfyty15kbLNZezGudjcKSqHHVoHQEeb9IJcjgQ6CiurrHZoEt47yce891vjGw==",
+ "license": "BSD-3-Clause"
+ },
"node_modules/dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@@ -1007,6 +1201,45 @@
"node": ">=8.6.0"
}
},
+ "node_modules/fast-xml-builder": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.3.1.tgz",
+ "integrity": "sha512-pIM/1n3ntFXKYrUZwW7QCK0gAW7XY+wzj1YMIV3tLDvPj/V+zTGJK5e3/4WJfwj0qWw2ElNXiTixda/R+3YSug==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "path-expression-matcher": "^1.6.2",
+ "xml-naming": "^0.3.0"
+ }
+ },
+ "node_modules/fast-xml-parser": {
+ "version": "5.11.2",
+ "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.11.2.tgz",
+ "integrity": "sha512-R9iDuNrQYeQut46cn2r2wHKn4HYzVDvDm5J1wW+koZewykv0yuO2HChTYeZtrULyHIDM9cj9TUXloCsueCQUog==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@nodable/entities": "^3.0.1",
+ "fast-xml-builder": "^1.2.0",
+ "is-unsafe": "^2.0.0",
+ "path-expression-matcher": "^1.6.2",
+ "strnum": "^2.4.2",
+ "xml-naming": "^0.3.0"
+ },
+ "bin": {
+ "fxparser": "src/cli/cli.js"
+ }
+ },
"node_modules/fastq": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
@@ -1073,6 +1306,67 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
+ "node_modules/geostyler-openlayers-parser": {
+ "version": "5.7.1",
+ "resolved": "file:vendor/geostyler-openlayers-parser-5.7.1.tgz",
+ "integrity": "sha512-TzkPiwy88voAZ5zmwVlG1jge8UM20nLZgAUy+usTk74lN/YKNvwg6q6cJDdhhfeR0Mt8c/dXaswKOmT48ykJNg==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "css-font-parser": "^2.0.0",
+ "geostyler-style": "^13.0.0"
+ },
+ "engines": {
+ "node": ">=20.6.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/geostyler"
+ },
+ "peerDependencies": {
+ "ol": ">=7.4"
+ }
+ },
+ "node_modules/geostyler-sld-parser": {
+ "version": "9.0.3",
+ "resolved": "https://registry.npmjs.org/geostyler-sld-parser/-/geostyler-sld-parser-9.0.3.tgz",
+ "integrity": "sha512-cgaBmmm+0DhT+cU59T4OCFA16ySJXtbw33uMRGROSaBFLcF8qnZf9/37J3crFwyobW1VVnUFiu/cU4YUT/V9Cw==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "fast-xml-parser": "^5.2.3",
+ "geostyler-style": "^12.0.0"
+ },
+ "engines": {
+ "node": ">=20.6.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/geostyler"
+ }
+ },
+ "node_modules/geostyler-sld-parser/node_modules/geostyler-style": {
+ "version": "12.0.0",
+ "resolved": "https://registry.npmjs.org/geostyler-style/-/geostyler-style-12.0.0.tgz",
+ "integrity": "sha512-teNaY/a4tVrGAt7vqvrXi0mWBg7OxIdUKAtI0oik/IlO1GfOK4JPERfT/Z+6m1FwNLVNOEx0VvZ2JiTSevPrwQ==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20.6.0",
+ "npm": ">=10.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/geostyler"
+ }
+ },
+ "node_modules/geostyler-style": {
+ "version": "13.0.0",
+ "resolved": "https://registry.npmjs.org/geostyler-style/-/geostyler-style-13.0.0.tgz",
+ "integrity": "sha512-YA+7Cwya+WOSi31JDy8ljoxGhm7udZO0I9StLfiIiVoAvOXau6RCQfQgmqszmiLAI8ex+UMwjKn1xK/Y7OXPMg==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20.19.0",
+ "npm": ">=10.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/geostyler"
+ }
+ },
"node_modules/geotiff": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/geotiff/-/geotiff-3.0.4.tgz",
@@ -1226,6 +1520,18 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-unsafe": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-2.0.2.tgz",
+ "integrity": "sha512-HgbIHPBH0KHHCcjLfGsCvhtPTVxjaAZlXjwdz7/GQC40SjSe4sfQsar8J5VFo8JOSbarkpV0OLG95bbaNd9aAQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/jsonfile": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
@@ -1353,6 +1659,21 @@
"integrity": "sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA==",
"license": "MIT"
},
+ "node_modules/path-expression-matcher": {
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz",
+ "integrity": "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
@@ -1628,6 +1949,27 @@
"node": ">=0.10.0"
}
},
+ "node_modules/strnum": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.2.tgz",
+ "integrity": "sha512-rDG3Ah4TV0k1hWvLSzkZtMmLN9+eS+h3knq4MP6A42Y3Yh5qGNnOUs1jJkoSr8FG5dsL28c7KgkIBzSEykqtuw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "anynum": "^1.0.1"
+ }
+ },
+ "node_modules/style-mod": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.4.tgz",
+ "integrity": "sha512-XXWIQt633/EpAFx8aZDOTjBzrCaGmhvEQlQo6MVPfa2OzO2cWo+4hV9h+6UkHYlXGfy+ODXKUdP7Pthmcu5ATw==",
+ "license": "MIT"
+ },
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
@@ -1830,6 +2172,12 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/w3c-keyname": {
+ "version": "2.2.8",
+ "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
+ "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
+ "license": "MIT"
+ },
"node_modules/web-worker": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz",
@@ -1849,6 +2197,42 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/xml-formatter": {
+ "version": "3.7.0",
+ "resolved": "https://registry.npmjs.org/xml-formatter/-/xml-formatter-3.7.0.tgz",
+ "integrity": "sha512-+8qTc3zv2UcJ1v9IsSIce37Dl4MQG14Cp7tWrwmy202UaI1wqRukw5QMX1JHsV+DX64yw77EgGsj2s5wGvuMbQ==",
+ "license": "MIT",
+ "dependencies": {
+ "xml-parser-xo": "^4.1.5"
+ },
+ "engines": {
+ "node": ">= 16"
+ }
+ },
+ "node_modules/xml-naming": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.3.0.tgz",
+ "integrity": "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/xml-parser-xo": {
+ "version": "4.1.6",
+ "resolved": "https://registry.npmjs.org/xml-parser-xo/-/xml-parser-xo-4.1.6.tgz",
+ "integrity": "sha512-mXbSNSM+rIwndoxSwmFa83bOdeP8G/cOGr7XvxA67X7ebCzoAuVez9JYDCDub3zRDNBZxIbfjuXLSsamr7NfwQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20"
+ }
+ },
"node_modules/xml-utils": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/xml-utils/-/xml-utils-1.10.2.tgz",
diff --git a/workshop/exercises/app/package.json b/workshop/exercises/app/package.json
index 78ad1d0..10491d1 100644
--- a/workshop/exercises/app/package.json
+++ b/workshop/exercises/app/package.json
@@ -10,9 +10,16 @@
"rollup-plugin-copy": "^3.5.0",
"vite": "^7.3.1"
},
- "dependencies": {
- "bootstrap": "^5.3.7",
- "ol": "^10.8.0",
- "proj4": "^2.20.3"
- }
+ "dependencies": {
+ "bootstrap": "^5.3.7",
+ "codemirror": "^6.0.1",
+ "@codemirror/lang-xml": "^6.1.0",
+ "@codemirror/state": "^6.5.0",
+ "@codemirror/language": "^6.11.0",
+ "geostyler-openlayers-parser": "file:vendor/geostyler-openlayers-parser-5.7.1.tgz",
+ "geostyler-sld-parser": "^9.0.3",
+ "ol": "^10.8.0",
+ "proj4": "^2.20.3",
+ "xml-formatter": "^3.6.3"
+ }
}
diff --git a/workshop/exercises/app/vendor/geostyler-openlayers-parser-5.7.1.tgz b/workshop/exercises/app/vendor/geostyler-openlayers-parser-5.7.1.tgz
new file mode 100644
index 0000000..53d989b
Binary files /dev/null and b/workshop/exercises/app/vendor/geostyler-openlayers-parser-5.7.1.tgz differ
diff --git a/workshop/exercises/mapfiles/geostyler.map b/workshop/exercises/mapfiles/geostyler.map
new file mode 100644
index 0000000..0e9fd8d
--- /dev/null
+++ b/workshop/exercises/mapfiles/geostyler.map
@@ -0,0 +1,125 @@
+MAP
+ NAME "GeoStylerDemo"
+ SIZE 800 800
+ EXTENT 26.668678 58.339241 26.796582 58.409410
+ PROJECTION
+ "EPSG:4326"
+ END
+ OUTPUTFORMAT
+ NAME "geojson"
+ DRIVER "OGR/GEOJSON"
+ MIMETYPE "application/json; subtype=geojson; charset=utf-8"
+ FORMATOPTION "FORM=SIMPLE"
+ FORMATOPTION "STORAGE=memory"
+ FORMATOPTION "LCO:NATIVE_MEDIA_TYPE=application/vnd.geo+json"
+ FORMATOPTION "USE_FEATUREID=true" # ensure GeoJSON output has an id property
+ END
+
+ # OpenLayers (ImageWMS with serverType 'mapserver') requests HiDPI images
+ # with MAP_RESOLUTION = 90 x device pixel ratio. MapServer scales symbol
+ # sizes by RESOLUTION / DEFRESOLUTION, so setting both to 90 keeps symbols
+ # at the same visual size on any screen, and matching the SLD sizes used
+ # for client-side styling.
+
+ RESOLUTION 90
+ DEFRESOLUTION 90
+
+ # CONFIG "MS_ERRORFILE" "/etc/mapserver/tmp/mapserver.log"
+
+ WEB
+ IMAGEPATH "/etc/mapserver/tmp/" # end-slash is important!
+ METADATA
+ "ows_enable_request" "*"
+ "ows_srs" "EPSG:4326 EPSG:3857"
+ "ows_use_default_extent_for_getfeature" "false"
+ "wfs_getfeature_formatlist" "geojson"
+ END
+ END
+
+ # points
+ INCLUDE "includes/notched-rect.sym"
+
+ # lines
+ INCLUDE "includes/arrow-wide.sym"
+
+ # polygons
+ INCLUDE "includes/square-spaced.sym"
+ INCLUDE "includes/wave.sym"
+
+ LAYER
+ NAME "square-spaced"
+ TYPE POLYGON
+ STATUS OFF
+ PROJECTION
+ "EPSG:4326"
+ END
+ TEMPLATE "ttt"
+ CONNECTIONTYPE OGR
+ CONNECTION "data/osm/landuse_a.fgb"
+ METADATA
+ "gml_include_items" "all"
+ "gml_featureid" "fid"
+ "gml_types" "auto"
+ END
+ FILTER ("[fclass]" = "scrub")
+ INCLUDE "includes/square-spaced.class"
+ END
+
+ LAYER
+ NAME "wave"
+ TYPE POLYGON
+ STATUS OFF
+ PROJECTION
+ "EPSG:4326"
+ END
+ TEMPLATE "ttt"
+ CONNECTIONTYPE OGR
+ CONNECTION "data/osm/water_a.fgb"
+ METADATA
+ "gml_include_items" "all"
+ "gml_featureid" "fid"
+ "gml_types" "auto"
+ END
+ FILTER ("[fclass]" = "riverbank")
+ INCLUDE "includes/wave.class"
+ END
+
+ LAYER
+ NAME "notched-rect"
+ TYPE POINT
+ STATUS OFF
+ PROJECTION
+ "EPSG:4326"
+ END
+ TEMPLATE "ttt"
+ CONNECTIONTYPE OGR
+ CONNECTION "data/osm/pois.fgb"
+ METADATA
+ "gml_include_items" "all"
+ "gml_featureid" "fid"
+ "gml_types" "auto"
+ END
+ FILTER ("[fclass]" = "restaurant")
+ INCLUDE "includes/notched-rect.class"
+ END
+
+ LAYER
+ NAME "arrow-wide"
+ TYPE LINE
+ STATUS OFF
+ PROJECTION
+ "EPSG:4326"
+ END
+ TEMPLATE "ttt"
+ CONNECTIONTYPE OGR
+ CONNECTION "data/osm/roads.fgb"
+ METADATA
+ "gml_include_items" "all"
+ "gml_featureid" "fid"
+ "gml_types" "auto"
+ END
+ FILTER ("[fclass]" = "tertiary")
+ INCLUDE "includes/arrow-wide.class"
+ END
+
+END
\ No newline at end of file
diff --git a/workshop/exercises/mapfiles/includes/arrow-wide.class b/workshop/exercises/mapfiles/includes/arrow-wide.class
new file mode 100644
index 0000000..0b6107e
--- /dev/null
+++ b/workshop/exercises/mapfiles/includes/arrow-wide.class
@@ -0,0 +1,8 @@
+CLASS
+ STYLE
+ SYMBOL "arrow-wide"
+ SIZE 10
+ GAP -30 # draw in direction of the line
+ COLOR 200 0 0
+ END
+END
\ No newline at end of file
diff --git a/workshop/exercises/mapfiles/includes/arrow-wide.sym b/workshop/exercises/mapfiles/includes/arrow-wide.sym
new file mode 100644
index 0000000..5803ce7
--- /dev/null
+++ b/workshop/exercises/mapfiles/includes/arrow-wide.sym
@@ -0,0 +1,11 @@
+SYMBOL
+ NAME "arrow-wide"
+ TYPE VECTOR
+ FILLED TRUE
+ POINTS
+ 0 0
+ 3 0.5
+ 0 1
+ 0 0
+ END
+END
\ No newline at end of file
diff --git a/workshop/exercises/mapfiles/includes/notched-rect.class b/workshop/exercises/mapfiles/includes/notched-rect.class
new file mode 100644
index 0000000..cc32122
--- /dev/null
+++ b/workshop/exercises/mapfiles/includes/notched-rect.class
@@ -0,0 +1,8 @@
+CLASS
+ STYLE
+ SYMBOL "notched-rect"
+ OUTLINECOLOR 0 0 0
+ COLOR "#96DCF8"
+ SIZE 16
+ END
+END
\ No newline at end of file
diff --git a/workshop/exercises/mapfiles/includes/notched-rect.sym b/workshop/exercises/mapfiles/includes/notched-rect.sym
new file mode 100644
index 0000000..681eb7c
--- /dev/null
+++ b/workshop/exercises/mapfiles/includes/notched-rect.sym
@@ -0,0 +1,28 @@
+SYMBOL
+ NAME "notched-rect"
+ TYPE VECTOR
+ FILLED TRUE
+ POINTS
+ 16 0
+ 64 0
+ 65.22 6.12
+ 68.69 11.31
+ 73.88 14.78
+ 80 16
+ 80 44
+ 73.88 45.22
+ 68.69 48.69
+ 65.22 53.88
+ 64 60
+ 16 60
+ 14.78 53.88
+ 11.31 48.69
+ 6.12 45.22
+ 0 44
+ 0 16
+ 6.12 14.78
+ 11.31 11.31
+ 14.78 6.12
+ 16 0
+ END
+END
\ No newline at end of file
diff --git a/workshop/exercises/mapfiles/includes/square-spaced.class b/workshop/exercises/mapfiles/includes/square-spaced.class
new file mode 100644
index 0000000..9c47e24
--- /dev/null
+++ b/workshop/exercises/mapfiles/includes/square-spaced.class
@@ -0,0 +1,12 @@
+CLASS
+ STYLE
+ COLOR "#FFC000"
+ SYMBOL "square-spaced"
+ WIDTH 12
+ SIZE 5
+ END
+ STYLE
+ OUTLINECOLOR "#FFC000"
+ WIDTH 2
+ END
+END
\ No newline at end of file
diff --git a/workshop/exercises/mapfiles/includes/square-spaced.sym b/workshop/exercises/mapfiles/includes/square-spaced.sym
new file mode 100644
index 0000000..5b267ce
--- /dev/null
+++ b/workshop/exercises/mapfiles/includes/square-spaced.sym
@@ -0,0 +1,12 @@
+SYMBOL
+ NAME "square-spaced"
+ TYPE VECTOR
+ FILLED TRUE
+ POINTS
+ 0.375 0.375
+ 0.625 0.375
+ 0.625 0.625
+ 0.375 0.625
+ 0.375 0.375
+ END
+END
\ No newline at end of file
diff --git a/workshop/exercises/mapfiles/includes/wave.class b/workshop/exercises/mapfiles/includes/wave.class
new file mode 100644
index 0000000..b29aecb
--- /dev/null
+++ b/workshop/exercises/mapfiles/includes/wave.class
@@ -0,0 +1,12 @@
+CLASS
+ STYLE
+ SYMBOL "wave"
+ COLOR "#4A90D9"
+ WIDTH 1
+ SIZE 14
+ END
+ STYLE
+ OUTLINECOLOR 0 0 0
+ WIDTH 1
+ END
+END
diff --git a/workshop/exercises/mapfiles/includes/wave.sym b/workshop/exercises/mapfiles/includes/wave.sym
new file mode 100644
index 0000000..a677092
--- /dev/null
+++ b/workshop/exercises/mapfiles/includes/wave.sym
@@ -0,0 +1,20 @@
+SYMBOL
+ NAME "wave"
+ TYPE VECTOR
+ FILLED FALSE
+ POINTS
+ 0 0.5
+ 0.083 0.675
+ 0.167 0.803
+ 0.25 0.85
+ 0.333 0.803
+ 0.417 0.675
+ 0.5 0.5
+ 0.583 0.325
+ 0.667 0.197
+ 0.75 0.15
+ 0.833 0.197
+ 0.917 0.325
+ 1 0.5
+ END
+END
\ No newline at end of file