@masterportal/masterportalapi 2.7.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,13 +5,24 @@
5
5
 
6
6
  ## Unreleased - in development
7
7
  ### Added
8
+
8
9
  ### Changed
9
10
 
10
11
  ### Deprecated
11
12
 
12
13
  ### Removed
14
+
13
15
  ### Fixed
14
16
 
17
+ ---
18
+
19
+ ## 2.8.0 - 2022-10-04
20
+ ### Added
21
+ - Issue #814: Adding optional error callbacks and availability detection to layers.
22
+ - Added possibility to set initial features for layer vectorBase.
23
+
24
+ ---
25
+
15
26
  ## 2.7.0 - 2022-09-14
16
27
  ### Changed
17
28
  - The following packages have been updated:
package/README.md CHANGED
@@ -10,6 +10,13 @@ If you want to create a 3d map, you have to provide the peer dependency cesium.
10
10
 
11
11
  By importing the project by module name like ``import ... from "masterportalAPI"``, most bundlers and bundler configurations will include the whole masterportalAPI. If you only need a subset of the provided functions and want to keep your build clean, directly import the needed functions like ``import {createMap} from "masterportalAPI/src/map.js``.
12
12
 
13
+ ## Error Handling
14
+
15
+ 1. Error event callback: The methods `createMap` and `addLayer` both optionally accept error event callbacks that are called with OpenLayers error events (tileloaderror, imageloaderror, featuresloaderror, error).
16
+ 2. Ping: A method that returns a Promise. This resolves to the status code of the layer's GetCapabilities request, which can be used to decide if the service is reachable and usable at all.
17
+
18
+ Both methods may be required for useful user feedback. For example, a WMS may answer a tile request with 404 if no tile is available for a region, but may be fully available within its specification. This may throw an error while the service itself is available as specified. It is up to the using implementation how to react, how often to ping, and so on.
19
+
13
20
  ## Scripts
14
21
 
15
22
  |Script|Effect|
@@ -47,7 +47,7 @@
47
47
  {
48
48
  "id": "4001",
49
49
  "name": "Gelaende",
50
- "url": "https://daten-hamburg.de/gdi3d/datasource-data/Gelaende ",
50
+ "url": "https://daten-hamburg.de/gdi3d/datasource-data/Gelaende",
51
51
  "typ": "Terrain3D",
52
52
  "cesiumTerrainProviderOptions": {
53
53
  "requestVertexNormals": true
package/example/index.js CHANGED
@@ -11,6 +11,10 @@ import portalConfig from "./config/portal.json";
11
11
  import localGeoJSON from "./config/localGeoJSON.js";
12
12
  import {load3DScript} from "../src/lib/load3DScript";
13
13
 
14
+ function errorCallback (errorEvent) {
15
+ console.error("This is an error event:", errorEvent);
16
+ }
17
+
14
18
  //* Add elements to window to play with API in console
15
19
  window.mpapi = {
16
20
  ...mpapi,
@@ -94,7 +98,7 @@ document.getElementById("layer-visibility").addEventListener("click", function (
94
98
  });
95
99
  });
96
100
 
97
- map2D = mapsAPI.map.createMap(config, "2D");
101
+ map2D = mapsAPI.map.createMap(config, "2D", {errorCallback});
98
102
  services.push(localService);
99
103
  // */
100
104
 
@@ -148,7 +152,18 @@ services.find(({id}) => id === "5001").style = styleOaf;
148
152
  //* SYNCHRONOUS EXAMPLE: layerConf is known
149
153
  window.mpapi.map = map2D;
150
154
 
151
- ["2001", "2002", "1002", "3001", "5001"].forEach(id => window.mpapi.map.addLayer(id));
155
+ ["2001", "2002", "1002", "3001", "5001"].forEach(id => window.mpapi.map.addLayer(id, {errorCallback}));
156
+
157
+ // ping all layers
158
+ services
159
+ .map(service => ({
160
+ ping: mpapi.ping(service),
161
+ service
162
+ }))
163
+ .forEach(({ping, service}) => ping
164
+ // eslint-disable-next-line no-console
165
+ .then(statusCode => console.log(`Service ${service.id} pinged; returned ${statusCode}.`))
166
+ .catch(console.error));
152
167
 
153
168
  // */
154
169
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@masterportal/masterportalapi",
3
3
  "author": "Implementierungspartnerschaft Masterportal <info@masterportal.org> (https://www.masterportal.org)",
4
- "version": "2.7.0",
4
+ "version": "2.8.0",
5
5
  "license": "MIT",
6
6
  "description": "Basic functions of the Masterportal as api",
7
7
  "repository": {
@@ -58,5 +58,9 @@
58
58
  },
59
59
  "bugs": {
60
60
  "url": "https://bitbucket.org/geowerkstatt-hamburg/masterportalapi/issues"
61
- }
61
+ },
62
+ "keywords": [
63
+ "geo",
64
+ "map"
65
+ ]
62
66
  }
package/src/index.js CHANGED
@@ -14,6 +14,7 @@ import Tileset from "./layer/tileset";
14
14
  import * as layerLib from "./layer/lib";
15
15
  import {search, setGazetteerUrl} from "./searchAddress";
16
16
  import setBackgroundImage from "./lib/setBackgroundImage";
17
+ import ping from "./lib/ping";
17
18
 
18
19
  export {
19
20
  createMapView,
@@ -31,6 +32,7 @@ export {
31
32
  setBackgroundImage,
32
33
  setGazetteerUrl,
33
34
  rawLayerList,
35
+ ping,
34
36
  search,
35
37
  crs
36
38
  };
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Helper function to detect layer availability/usability. Depending on layer
3
+ * type, the method to infer availability broadly varies.
4
+ * @param {Object} layerSpecification services.json layer specification
5
+ * @returns {Promise} resolves to status code of a layer-specific default
6
+ * request as Number, or null if no request could be made, which should
7
+ * indicate misconfiguration; defaults to 900 if the request itself failed for
8
+ * arbitrary reasons (usually CORS-related)
9
+ */
10
+ export default function ({url, typ, capabilitiesUrl, ...rest}) {
11
+ const statusCheckUrls = [];
12
+
13
+ if (capabilitiesUrl) {
14
+ statusCheckUrls.push(capabilitiesUrl);
15
+ }
16
+ else if (typ === "OAF") {
17
+ statusCheckUrls.push(url);
18
+ }
19
+ else if (typ === "Entities3D") {
20
+ rest.entities
21
+ .forEach(entity => entity.url ? statusCheckUrls.push(entity.url) : null);
22
+ }
23
+ else if (typ === "TileSet3D" || typ === "Terrain3D") {
24
+ statusCheckUrls.push(url);
25
+ }
26
+ else if (typ === "GeoJSON") {
27
+ if (!url && rest.features) {
28
+ // may be a local layer without source url – that's equal to 200
29
+ return Promise.resolve(200);
30
+ }
31
+ if (url) {
32
+ statusCheckUrls.push(url);
33
+ }
34
+ }
35
+ else if (typ === "VectorTile") {
36
+ statusCheckUrls.push(url.replaceAll(/\{[xyz]\}/ig, "0"));
37
+ }
38
+ else if (typ === "WMTS") {
39
+ const arbitraryTileRequests = (url ? [url] : rest.urls)
40
+ .map(entry => entry
41
+ .replace(/\{Style\}/ig, rest.style)
42
+ .replace(/\{TileMatrixSet\}/ig, rest.tileMatrixSet)
43
+ .replaceAll(/\{(TileMatrix|TileRow|TileCol)\}/ig, "0"));
44
+
45
+ statusCheckUrls.push(...arbitraryTileRequests);
46
+ }
47
+ else if (url) {
48
+ statusCheckUrls.push(`${url}?service=${typ}&request=GetCapabilities`);
49
+ }
50
+
51
+ if (!statusCheckUrls.length) {
52
+ return Promise.resolve(null);
53
+ }
54
+
55
+ return Promise
56
+ .allSettled(statusCheckUrls
57
+ .map(statusCheckUrl => new Promise((resolve) => fetch(statusCheckUrl, {method: "HEAD"})
58
+ .then(({status}) => resolve(status))
59
+ // usually from CORS, treated as 900 here ("proprietary" status code)
60
+ .catch(() => resolve(900)))))
61
+ // return highest status code (will indicate erroneous behaviour)
62
+ .then(codes => Math.max(...codes.map(({value}) => value)));
63
+ }
package/src/maps/map.js CHANGED
@@ -20,6 +20,7 @@ export default {
20
20
  * @param {object} [settings={}] - settings object
21
21
  * @param {object} [settings.mapParams] - additional parameter object that is spread into the ol.Map constructor object
22
22
  * @param {function} [settings.callback] - optional callback for layer list loading
23
+ * @param {function} [settings.errorCallback] - optional callback for layer error events
23
24
  * @returns {module:ol/Map~Map} The map.
24
25
  */
25
26
  createMap: function (config, mapMode = "2D", settings = {}) {
@@ -32,6 +32,48 @@ const layerBuilderMap = {
32
32
  },
33
33
  originalAddLayer = Map.prototype.addLayer;
34
34
 
35
+ /**
36
+ * Flattens a Layer/LayerGroup to an array of non-group instances.
37
+ * @param {ol/layer/Base} layer any layer, possibly a LayerGroup
38
+ * @returns {ol/layer/Base[]} array of all given layers
39
+ */
40
+ function flattenLayerGroups (layer) {
41
+ return layer.getLayers
42
+ ? layer
43
+ .getLayers()
44
+ .getArray()
45
+ .map(l => flattenLayerGroups(l))
46
+ .flat(1)
47
+ : [layer];
48
+ }
49
+
50
+ /**
51
+ * Adds an error handling function to an arbitrary layer. The event will be
52
+ * registered to the tileloaderror, imageloaderror, featuresloaderror, and
53
+ * generic error event. Depending on the type of layer and error, only one of
54
+ * these events will fire.
55
+ * @param {ol/layer/Base} layer Any type of layer implemented.
56
+ * @param {Function} errorCallback Error callback, called from OL event.
57
+ * @returns {void} side-effect to layer
58
+ */
59
+ function injectErrorCallback (layer, errorCallback) {
60
+ const layers = flattenLayerGroups(layer);
61
+
62
+ layers.forEach(l => {
63
+ const source = l.getSource?.();
64
+
65
+ if (source) {
66
+ source.on?.("tileloaderror", errorCallback);
67
+ source.on?.("imageloaderror", errorCallback);
68
+ source.on?.("featuresloaderror", errorCallback);
69
+ source.on?.("error", errorCallback);
70
+ }
71
+ else {
72
+ console.error("Could not register error callback on layer:", l);
73
+ }
74
+ });
75
+ }
76
+
35
77
  /**
36
78
  * Adds a layer to the map, or adds a layer to the map by id.
37
79
  * This id is looked up within the array of all known services.
@@ -40,14 +82,18 @@ const layerBuilderMap = {
40
82
  * if you request the services from the internet.
41
83
  *
42
84
  * This function is available on all ol/Map instances.
43
- * @param {(string|ol/Layer)} layerOrId - if of layer to add to map
85
+ * @param {(string|ol/layer/Base)} layerOrId - if of layer to add to map
44
86
  * @param {object} [params] - optional parameter object
45
87
  * @param {boolean} [params.visibility=true] - whether added layer is initially visible
46
88
  * @param {Number} [params.transparency=0] - how visible the layer is initially
89
+ * @param {Function} [params.errorCallback=console.error] - callback for layer source error events
47
90
  * @returns {?ol.Layer} added layer
48
91
  */
49
- function addLayer (layerOrId, params = {visibility: true, transparency: 0}) {
50
- var layer, layerBuilder;
92
+ function addLayer (layerOrId, params = {visibility: true, transparency: 0, errorCallback: console.error}) {
93
+ const errorCallback = typeof params.errorCallback === "function"
94
+ ? params.errorCallback
95
+ : console.error;
96
+ let layer, layerBuilder;
51
97
 
52
98
  // if parameter is id, create and add layer with masterportalAPI mechanisms
53
99
  if (typeof layerOrId === "string") {
@@ -65,11 +111,13 @@ function addLayer (layerOrId, params = {visibility: true, transparency: 0}) {
65
111
  layer = layerBuilder.createLayer(rawLayer, {}, {map: this});
66
112
  layer.setVisible(typeof params.visibility === "boolean" ? params.visibility : true);
67
113
  layer.setOpacity(typeof params.transparency === "number" ? (100 - params.transparency) / 100 : 1);
114
+ injectErrorCallback(layer, errorCallback);
68
115
  originalAddLayer.call(this, layer);
69
116
  return layer;
70
117
  }
71
118
 
72
119
  // else use original function
120
+ injectErrorCallback(layerOrId, errorCallback);
73
121
  return originalAddLayer.call(this, layerOrId);
74
122
  }
75
123
 
@@ -93,10 +141,11 @@ Map.prototype.addLayer = addLayer;
93
141
  * @param {object} [settings={}] - setings object
94
142
  * @param {object} [settings.mapParams] - additional parameter object that is spread into the ol.Map constructor object
95
143
  * @param {function} [settings.callback] - optional callback for layer list loading
144
+ * @param {function} [settings.errorCallback] – method called on error events
96
145
  * @param {String} [mapMode = "2D"] The map mode. '2D' to craete a 2D-map and '3D' to create a 3D-map.
97
146
  * @returns {object} map object from ol
98
147
  */
99
- export function createMap (config = defaults, {mapParams, callback} = {}) {
148
+ export function createMap (config = defaults, {mapParams, callback, errorCallback} = {}) {
100
149
  registerProjections(config.namedProjections);
101
150
  setBackgroundImage(config);
102
151
  setGazetteerUrl(config.gazetteerUrl);
@@ -114,7 +163,7 @@ export function createMap (config = defaults, {mapParams, callback} = {}) {
114
163
  initializeLayerList(config.layerConf, (param, error) => {
115
164
  getInitialLayers(config)
116
165
  .forEach(layer => {
117
- map.addLayer(layer.id, layer);
166
+ map.addLayer(layer.id, {errorCallback});
118
167
  });
119
168
 
120
169
  if (typeof callback === "function") {
@@ -3,7 +3,7 @@ import {getWidth} from "ol/extent";
3
3
 
4
4
  import * as wmts from "../../src/layer/wmts";
5
5
 
6
- describe.only("wmts.js", function () {
6
+ describe("wmts.js", function () {
7
7
  describe("generateArrays", function () {
8
8
  it("should fill the arrays resolutions and matrixIds with numbers", function () {
9
9
  const size = getWidth(getProjection("EPSG:3857").getExtent()) / 256,
@@ -0,0 +1,69 @@
1
+ import ping from "../../src/lib/ping";
2
+
3
+ const originalFetch = global.fetch;
4
+
5
+ describe("ping.js", function () {
6
+ let mockFetch;
7
+
8
+ beforeEach(() => {
9
+ mockFetch = jest.fn(() => Promise.resolve({
10
+ status: 200
11
+ }));
12
+ global.fetch = mockFetch;
13
+ });
14
+
15
+ afterAll(() => {
16
+ global.fetch = originalFetch;
17
+ });
18
+
19
+ describe("ping", function () {
20
+ it("calls an url from the service definition", async function () {
21
+ const status = await ping({url: "www.example.com", typ: "WMS"});
22
+
23
+ expect(status).toBe(200);
24
+ expect(mockFetch).toHaveBeenCalledWith("www.example.com?service=WMS&request=GetCapabilities", {method: "HEAD"});
25
+ });
26
+
27
+ it("calls 0/0/0 tile for VectorTile services", async function () {
28
+ const status = await ping({url: "www.example.com/{x}/{y}/{z}", typ: "VectorTile"});
29
+
30
+ expect(status).toBe(200);
31
+ expect(mockFetch).toHaveBeenCalledWith("www.example.com/0/0/0", {method: "HEAD"});
32
+ });
33
+
34
+ it("calls the URL directly for OAF services", async function () {
35
+ const status = await ping({url: "www.example.com", typ: "OAF"});
36
+
37
+ expect(status).toBe(200);
38
+ expect(mockFetch).toHaveBeenCalledWith("www.example.com", {method: "HEAD"});
39
+ });
40
+
41
+ it("returns 900 on network errors", async function () {
42
+ mockFetch = jest.fn(() => Promise.resolve({status: 900}));
43
+ global.fetch = mockFetch;
44
+
45
+ const status = await ping({url: "www.example.com", typ: "OAF"});
46
+
47
+ expect(status).toBe(900);
48
+ expect(mockFetch).toHaveBeenCalledWith("www.example.com", {method: "HEAD"});
49
+ });
50
+
51
+ it("returns highest status code on multiple urls", async function () {
52
+ let counter = 200;
53
+
54
+ mockFetch = jest.fn(() => Promise.resolve({status: counter++}));
55
+ global.fetch = mockFetch;
56
+
57
+ const status = await ping({urls: [
58
+ "www.example.com",
59
+ "www.example.com/but"
60
+ ], typ: "WMTS"});
61
+
62
+ expect(status).toBe(201);
63
+ expect(mockFetch)
64
+ .toHaveBeenCalledWith("www.example.com", {method: "HEAD"});
65
+ expect(mockFetch)
66
+ .toHaveBeenCalledWith("www.example.com/but", {method: "HEAD"});
67
+ });
68
+ });
69
+ });