Skip to content

Commit 8f250de

Browse files
authored
Merge pull request #4 from geographika/arcgis-workshop
Add ArcGIS FeatureServer example
2 parents c4c0027 + 6df4189 commit 8f250de

File tree

6 files changed

+365
-0
lines changed

6 files changed

+365
-0
lines changed
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
# Working with an ArcGIS Feature Server
2+
3+
## Overview
4+
5+
MapServer can read JSON data from an ArcGIS Feature Server using GDAL's [ESRIJSON / FeatureService](https://gdal.org/en/stable/drivers/vector/esrijson.html) driver.
6+
You can render data, configure WMS services and apply labels just as you would with any other MapServer data source.
7+
8+
In this workshop, you will also learn how to add a checkbox control to an OpenLayers map that allows users to toggle labels on and off interactively.
9+
10+
<div class="map">
11+
<iframe src="https://mapserver.github.io/getting-started-with-mapserver-demo/arcgis.html"></iframe>
12+
</div>
13+
14+
## Inspecting the Data
15+
16+
We can inspect the ArcGIS Feature Server data using GDAL tools available in the MapServer Docker container.
17+
This helps verify that the connection and driver are working correctly before configuring MapServer.
18+
Run the following command to open your container shell and get information about the service:
19+
20+
```bash
21+
docker exec -it mapserver /bin/bash
22+
gdal vector info "https://sampleserver6.arcgisonline.com/arcgis/rest/services/PoolPermits/FeatureServer/0/query?resultRecordCount=10&f=pjson" --output-format text
23+
```
24+
25+
The output should look similar to the following:
26+
27+
```bash
28+
INFO: Open of `https://sampleserver6.arcgisonline.com/arcgis/rest/services/PoolPermits/FeatureServer/0/query?resultRecordCount=10&f=pjson'
29+
using driver `ESRIJSON' successful.
30+
31+
Layer name: ESRIJSON
32+
Geometry: Polygon
33+
Feature Count: 983
34+
Extent: (-117.462057, 33.895445) - (-117.436808, 33.911090)
35+
Layer SRS WKT:
36+
PROJCRS["WGS 84 / Pseudo-Mercator",
37+
...
38+
ID["EPSG",3857]]
39+
```
40+
41+
This output tells us two important things:
42+
43+
- The data extent (bounding box) in geographic coordinates - (-117.462057, 33.895445) to (-117.436808, 33.911090).
44+
- The spatial reference system - EPSG:3857 (WGS 84 / Pseudo-Mercator).
45+
46+
We can use the extent values and projection in our Mapfile as below:
47+
48+
```scala
49+
MAP
50+
NAME "arcgis"
51+
EXTENT -117.462057 33.895445 -117.436808 33.911090
52+
PROJECTION
53+
"init=epsg:4326"
54+
END
55+
```
56+
57+
Although the Mapfile's extent here is expressed in EPSG:4326 (latitude/longitude), our OpenLayers client will use Web Mercator (EPSG:3857) coordinates.
58+
To handle this, we can use a small MapScript Python utility to read the extent from the Mapfile and convert it to Web Mercator automatically.
59+
60+
61+
```bash
62+
$ python /scripts/extents.py --mapfile "/etc/mapserver/arcgis.map"
63+
Original extent +init=epsg:4326: [-117.462057, 33.895445, -117.436808, 33.91109]
64+
New extent epsg:3857: [-13075816.372770477, 4014771.4694313034, -13073005.666947436, 4016869.8241438307]
65+
Center: [-13074411.019858956, 4015820.646787567]
66+
```
67+
68+
We can then use these projected coordinates in our OpenLayers client application to set the map's center and initial view to match the location of our data:
69+
70+
```js
71+
const map = new Map({
72+
...
73+
view: new View({
74+
center: [-13074410.5, 4015820],
75+
zoom: 17,
76+
}),
77+
});
78+
```
79+
80+
Finally, we'll want to check which attribute fields are available in the dataset so we can choose one to use for labeling.
81+
We can inspect the dataset details in JSON format using the ArcGIS Feature Server's REST endpoint:
82+
83+
```bash
84+
gdal vector info --summary "https://sampleserver6.arcgisonline.com/arcgis/rest/services/PoolPermits/FeatureServer/0/query?resultRecordCount=10&f=pjson" --output-format json
85+
...
86+
"featureCount":983,
87+
"fields":[
88+
{
89+
"name":"apn",
90+
"type":"String",
91+
"width":9,
92+
"nullable":true,
93+
"uniqueConstraint":false,
94+
"alias":"APN"
95+
}
96+
]
97+
}
98+
```
99+
100+
The only available attribute field in this dataset is `apn`. Since it is a string, we can use it directly for labeling features on the map.
101+
102+
## The Mapfile
103+
104+
The `LAYER` uses `CONNECTIONTYPE OGR` and points directly to the ArcGIS FeatureServer, including `f=pjson` in the query string:
105+
106+
```scala
107+
CONNECTIONTYPE OGR
108+
CONNECTION "https://sampleserver6.arcgisonline.com/arcgis/rest/services/PoolPermits/FeatureServer/0/query?f=pjson"
109+
```
110+
111+
To toggle labels on and off using a query string parameter (`labels`) we make use of MapServer [runtime substitution](https://mapserver.org/cgi/runsub.html).
112+
113+
1. Add a `labels` parameter to the `CLASS` `VALIDATION` block and set its default value to `'hidden'`.
114+
2. Add an `EXPRESSION` in the class containing the labels that evaluates to **True** when `'visible' = 'visible'` and **False** when `'hidden' = 'visible'`.
115+
116+
Using this mechanism, labels can be shown for features by appending `&LABELS=visible` to the request URL. By default they will be hidden.
117+
118+
A final point is the `PROCESSING "RENDERMODE=ALL_MATCHING_CLASSES"` directive.
119+
120+
- By default, MapServer applies **only the first matching class** for each feature.
121+
- With `ALL_MATCHING_CLASSES`, **each feature is evaluated against every class**, allowing multiple classes and styles can be applied - in this case a polygon and then a label.
122+
123+
An alternative approach to the above would be to use two layers - one to render the polygons, and another to render just the labels. The client application could then request **one or both**
124+
layers via WMS. The best approach depends on the application requirements.
125+
126+
```scala
127+
PROCESSING "RENDERMODE=ALL_MATCHING_CLASSES"
128+
CLASS
129+
...
130+
END
131+
CLASS
132+
VALIDATION
133+
labels '.'
134+
default_labels 'hidden'
135+
END
136+
EXPRESSION ('%labels%' = 'visible')
137+
LABEL
138+
...
139+
END
140+
```
141+
142+
## OpenLayers
143+
144+
The OpenLayers client needs a way to toggle labels on and off. In `arcgis.html` we add a simple HTML checkbox and apply CSS to position it in a panel in the bottom-left corner:
145+
146+
```html
147+
<div id="control-panel">
148+
<label>
149+
<input type="checkbox" id="labelsCheckbox" />
150+
Labels
151+
</label>
152+
</div>
153+
```
154+
155+
In the JavaScript file (`arcgis.js`) we then add an event listener that triggers whenever the checkbox state changes. This function:
156+
157+
1. Updates the WMS layer parameters sent to MapServer to include the LABELS query parameter.
158+
2. Forces the WMS layer to refresh, so the labels are rendered or hidden immediately.
159+
160+
```js
161+
const labelsCheckbox = document.getElementById('labelsCheckbox');
162+
labelsCheckbox.addEventListener('change', (event) => {
163+
const showLabels = event.target.checked ? 'visible' : 'hidden';
164+
// update the WMS parameters
165+
imageLayer.getSource().updateParams({ LABELS: showLabels });
166+
167+
// refresh the layer
168+
imageLayer.getSource().refresh();
169+
});
170+
```
171+
172+
## Code
173+
174+
!!! example
175+
176+
- Direct MapServer request: <http://localhost:7000/?map=/etc/mapserver/arcgis.map&REQUEST=GetMap&SERVICE=WMS&VERSION=1.3.0&FORMAT=image%2Fpng&STYLES=&TRANSPARENT=TRUE&LAYERS=PoolPermits&WIDTH=3840&HEIGHT=1907&CRS=EPSG%3A3857&BBOX=-13076703%2C4014686%2C-13072117.389151445%2C4016958>
177+
- Direct MapServer request with labels: <http://localhost:7000/?map=/etc/mapserver/arcgis.map&REQUEST=GetMap&SERVICE=WMS&VERSION=1.3.0&FORMAT=image%2Fpng&STYLES=&TRANSPARENT=TRUE&LAYERS=PoolPermits&LABELS=visible&WIDTH=3840&HEIGHT=1907&CRS=EPSG%3A3857&BBOX=-13076703%2C4014686%2C-13072117.389151445%2C4016958>
178+
- Local OpenLayers example: <http://localhost:7001/arcgis.html>
179+
180+
??? JavaScript "arcgis.js"
181+
182+
``` js
183+
--8<-- "arcgis.js"
184+
```
185+
186+
??? Mapfile "stac.map"
187+
188+
``` scala
189+
--8<-- "arcgis.map"
190+
```
191+
192+
## Exercises
193+
194+
In this exercise, you will debug the application using `map2img` to compare performance between using a single layer (polygons + labels) and two layers (polygons and labels separate).
195+
196+
!!! note
197+
198+
To ensure the labels are drawn when using a single layer, temporarily comment out the `EXPRESSION` block to ensure the labels are drawn.
199+
There is currently no way to add custom parameters to `map2img`. Remember to add this back when drawing two layers to avoid rendering the labels twice.
200+
201+
```bash
202+
docker exec -it mapserver /bin/bash
203+
204+
# test a single layer with polygons and labels
205+
map2img -m arcgis.map -l "PoolPermits" -layer_debug "PoolPermits" 1 -map_debug 5 -o PoolPermits.png
206+
207+
# test two layers - one polygons and the other labels
208+
map2img -m arcgis.map -l "PoolPermits" "PoolPermitLabels" -layer_debug "PoolPermits" 1 -layer_debug "PoolPermitLabels" 1 -map_debug 5 -o PoolPermit2Layers.png
209+
210+
# draw map 5 times to get several map drawing times
211+
map2img -m arcgis.map -l "PoolPermits" -c 5 -map_debug 2 -o temp.png
212+
```
213+
214+
The generated images will appear in the same folder as your Mapfiles on your local machine: `getting-started-with-mapserver/workshop/exercises/mapfiles`.
215+
Verify that the images are identical to ensure you are comparing the same outputs.
216+
217+
What are your timings for each approach?
218+
219+
<!--
220+
Times should be almost identical as MapServer reuses the connection to the remote datasource for each layer.
221+
msConnPoolRegister(PoolPermits,https://sampleserver6.arcgisonline.com/arcgis/rest/services/PoolPermits/FeatureServer/0/query?f=pjson,0x5f5ff6e11570)
222+
-->

workshop/content/mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ nav:
2929
- Vector Tiles: outputs/vector-tiles.md
3030
- OGC API - Features: outputs/ogcapi-features.md
3131
- Advanced:
32+
- ArcGIS Feature Server: advanced/arcgis.md
3233
- Vector Symbols: advanced/symbols.md
3334
- Clusters: advanced/clusters.md
3435
- SLD: advanced/sld.md

workshop/exercises/app/arcgis.html

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link rel="icon" type="image/x-icon" href="https://openlayers.org/favicon.ico" />
6+
<link rel="stylesheet" href="node_modules/ol/ol.css">
7+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
8+
<title>ArcGIS Feature Server</title>
9+
<style>
10+
/* panel styling */
11+
#control-panel {
12+
position: absolute;
13+
bottom: 10px;
14+
left: 10px;
15+
background: rgba(255, 255, 255, 0.9);
16+
padding: 8px 12px;
17+
border-radius: 5px;
18+
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
19+
font-family: sans-serif;
20+
font-size: 14px;
21+
}
22+
23+
#control-panel input[type="checkbox"] {
24+
margin-right: 6px;
25+
}
26+
</style>
27+
</head>
28+
<body>
29+
<div id="map"></div>
30+
<div id="control-panel">
31+
<label>
32+
<input type="checkbox" id="labelsCheckbox" />
33+
Labels
34+
</label>
35+
</div>
36+
<script type="module" src="./js/arcgis.js"></script>
37+
</body>
38+
</html>

workshop/exercises/app/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ <h2>Outputs</h2>
3232
</ul>
3333
<h2>Advanced</h2>
3434
<ul>
35+
<li><a href="arcgis.html">ArcGIS Feature Server</a></li>
3536
<li><a href="railways.html">Vector Symbols (Railways)</a></li>
3637
<li><a href="clusters.html">Clusters</a></li>
3738
<li><a href="landuse.html">Landuse</a></li>
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import '../css/style.css';
2+
import ImageWMS from 'ol/source/ImageWMS.js';
3+
import Map from 'ol/Map.js';
4+
import OSM from 'ol/source/OSM.js';
5+
import View from 'ol/View.js';
6+
import { Image as ImageLayer, Tile as TileLayer } from 'ol/layer.js';
7+
8+
const mapserverUrl = import.meta.env.VITE_MAPSERVER_BASE_URL;
9+
const mapfilesPath = import.meta.env.VITE_MAPFILES_PATH;
10+
11+
const imageLayer = new ImageLayer({
12+
source: new ImageWMS({
13+
url: mapserverUrl + mapfilesPath + 'arcgis.map&',
14+
params: { 'LAYERS': 'PoolPermits', 'STYLES': '', LABELS: 'hidden' }
15+
}),
16+
});
17+
const layers = [
18+
new TileLayer({
19+
source: new OSM(),
20+
opacity: 0.2,
21+
className: 'bw'
22+
}),
23+
imageLayer
24+
];
25+
const map = new Map({
26+
layers: layers,
27+
target: 'map',
28+
view: new View({
29+
center: [-13074410.5, 4015820],
30+
zoom: 17,
31+
}),
32+
});
33+
34+
const labelsCheckbox = document.getElementById('labelsCheckbox');
35+
labelsCheckbox.addEventListener('change', (event) => {
36+
const showLabels = event.target.checked ? 'visible' : 'hidden';
37+
// update the WMS parameters
38+
imageLayer.getSource().updateParams({ LABELS: showLabels });
39+
40+
// refresh the layer
41+
imageLayer.getSource().refresh();
42+
});
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
MAP
2+
NAME "arcgis"
3+
EXTENT -117.462057 33.895445 -117.436808 33.911090
4+
SIZE 400 400
5+
PROJECTION
6+
"init=epsg:4326"
7+
END
8+
WEB
9+
METADATA
10+
"ows_enable_request" "*"
11+
"ows_srs" "EPSG:4326 EPSG:3857"
12+
END
13+
END
14+
LAYER
15+
NAME "PoolPermits"
16+
TYPE POLYGON
17+
PROJECTION
18+
"init=epsg:3857"
19+
END
20+
CONNECTIONTYPE OGR
21+
CONNECTION "https://sampleserver6.arcgisonline.com/arcgis/rest/services/PoolPermits/FeatureServer/0/query?f=pjson"
22+
PROCESSING "RENDERMODE=ALL_MATCHING_CLASSES"
23+
CLASS
24+
STYLE
25+
COLOR 0 173 181
26+
OUTLINECOLOR 230 230 230
27+
OUTLINEWIDTH 0.1
28+
END
29+
END
30+
CLASS
31+
VALIDATION
32+
labels '.'
33+
default_labels 'hidden'
34+
END
35+
EXPRESSION ('%labels%' = 'visible')
36+
LABEL
37+
TEXT "[apn]"
38+
COLOR 220 240 255
39+
SIZE 8
40+
END
41+
END
42+
END
43+
44+
# this layer is used for the excercise only - not in the arcgis.html page
45+
LAYER
46+
NAME "PoolPermitLabels"
47+
TYPE POLYGON
48+
PROJECTION
49+
"init=epsg:3857"
50+
END
51+
CONNECTIONTYPE OGR
52+
CONNECTION "https://sampleserver6.arcgisonline.com/arcgis/rest/services/PoolPermits/FeatureServer/0/query?f=pjson"
53+
CLASS
54+
LABEL
55+
TEXT "[apn]"
56+
COLOR 220 240 255
57+
SIZE 8
58+
END
59+
END
60+
END
61+
END

0 commit comments

Comments
 (0)