@masterportal/masterportalapi 2.62.0 → 2.64.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.
@@ -0,0 +1,216 @@
1
+ /* eslint-disable no-underscore-dangle */
2
+ import {GPX} from "ol/format.js";
3
+ import {Circle} from "ol/geom.js";
4
+ import {fromCircle} from "ol/geom/Polygon.js";
5
+ import {WKT} from "ol/format.js";
6
+ import Feature from "ol/Feature.js";
7
+ import {isObject, transformPoint, transformGeometry} from "./utils.js";
8
+
9
+ /**
10
+ * Transforms the given line or polygon coordinates from a given source projection to EPSG:4326.
11
+ *
12
+ * @param {String} sourceProjectionCode Source projection code (e.g. "EPSG:25832").
13
+ * @param {(Array<number>|Array<Array<number>>|Array<Array<Array<number>>>)} coords Coordinates.
14
+ * @param {Boolean} isPolygon Determines whether the given coordinates are a polygon or a line.
15
+ * @returns {(Array<number>|Array<Array<number>>|Array<Array<Array<number>>>)} Transformed coordinates.
16
+ */
17
+ function transform (sourceProjectionCode, coords, isPolygon) {
18
+ const transCoords = [];
19
+
20
+ for (const value of coords) {
21
+ if (isPolygon) {
22
+ if (coords.length > 1) {
23
+ transCoords.push(transform(sourceProjectionCode, value, isPolygon)[0]);
24
+ }
25
+ else {
26
+ value.forEach(point => {
27
+ if (point.length > 2) {
28
+ transCoords.push(transform(sourceProjectionCode, point, isPolygon));
29
+ }
30
+ else {
31
+ transCoords.push(transformPoint(sourceProjectionCode, point));
32
+ }
33
+ });
34
+ continue;
35
+ }
36
+ }
37
+ else if (value.length > 2 && !isPolygon) {
38
+ transCoords.push(transform(sourceProjectionCode, value, isPolygon));
39
+ }
40
+ else {
41
+ transCoords.push(transformPoint(sourceProjectionCode, value));
42
+ }
43
+ }
44
+ return isPolygon ? [transCoords] : transCoords;
45
+ }
46
+
47
+ /**
48
+ * Transforms the given geometry from a source projection to EPSG:4326.
49
+ * If the geometry type is not supported (LineString, Point, or Polygon), an empty array is returned.
50
+ *
51
+ * @param {module:ol/geom/Geometry} geometry Geometry to be transformed.
52
+ * @param {String} projection The source EPSG projection code.
53
+ * @returns {(Array<number>|Array<Array<number>>|Array<Array<Array<number>>>)} The transformed coordinates or an empty array.
54
+ */
55
+ function transformCoordinates (geometry, projection) {
56
+ const coords = geometry.getCoordinates(),
57
+ type = geometry.getType();
58
+
59
+ switch (type) {
60
+ case "LineString":
61
+ return transform(projection, coords, false);
62
+ case "Point":
63
+ return transformPoint(projection, coords);
64
+ case "Polygon":
65
+ return transform(projection, coords, true);
66
+ default:
67
+ return [];
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Adds draw state extensions to GPX feature nodes.
73
+ * @param {String} gpxData The generated GPX document.
74
+ * @param {ol/Feature[]} features The features written to the document.
75
+ * @returns {String} The GPX document with draw state extensions.
76
+ */
77
+ function addGpxDrawStateExtensions (gpxData, features) {
78
+ const document = new DOMParser().parseFromString(gpxData, "text/xml"),
79
+ featureNodes = Array.from(document.getElementsByTagName("*"))
80
+ .filter(node => ["wpt", "rte", "trk"].includes(node.localName)),
81
+ drawStateNamespace = "https://masterportal.org/gpx";
82
+
83
+ featureNodes.forEach((featureNode, index) => {
84
+ const drawState = features[index].get("masterportal_attributes")?.drawState;
85
+
86
+ if (!isObject(drawState)) {
87
+ return;
88
+ }
89
+
90
+ const extensions = document.createElementNS(document.documentElement.namespaceURI, "extensions"),
91
+ drawStateNode = document.createElementNS(drawStateNamespace, "masterportal:drawState");
92
+
93
+ drawStateNode.textContent = JSON.stringify(drawState);
94
+ extensions.appendChild(drawStateNode);
95
+ featureNode.appendChild(extensions);
96
+ document.documentElement.setAttribute("xmlns:masterportal", drawStateNamespace);
97
+ });
98
+
99
+ return new XMLSerializer().serializeToString(document);
100
+ }
101
+
102
+ /**
103
+ * Converts the features from OpenLayers Features to features in the chosen format,
104
+ * transforming coordinates from the given projection to EPSG:4326.
105
+ *
106
+ * @param {ol/Feature[]} features The features to be converted.
107
+ * @param {module:ol/format} format Format in which the features should be written.
108
+ * @param {String} projection The source EPSG projection code.
109
+ * @returns {String} The features written in the chosen format as a String.
110
+ */
111
+ function convertFeatures (features, format, projection) {
112
+ const convertedFeatures = [];
113
+ let notSupportedGeometryType = false;
114
+
115
+ for (const feature of features) {
116
+ const cloned = feature.clone(),
117
+ transCoords = transformCoordinates(cloned.getGeometry(), projection);
118
+
119
+ if (transCoords.length === 3 && transCoords[2] === 0) {
120
+ transCoords.pop();
121
+ }
122
+
123
+ cloned.getGeometry().setCoordinates(transCoords, "XY");
124
+ convertedFeatures.push(cloned);
125
+
126
+ if (format instanceof GPX && feature.getGeometry().getType() === "Polygon") {
127
+ notSupportedGeometryType = true;
128
+ }
129
+ }
130
+
131
+ if (notSupportedGeometryType) {
132
+ console.warn("Some features could not be exported because their geometry type is not supported by the chosen format.");
133
+ }
134
+
135
+ const dataString = format.writeFeatures(convertedFeatures);
136
+
137
+ return format instanceof GPX ? addGpxDrawStateExtensions(dataString, features) : dataString;
138
+ }
139
+
140
+ /**
141
+ * Prepares features for download by cloning them and converting Circle geometries
142
+ * to Polygons, storing circle metadata in masterportal_attributes.
143
+ *
144
+ * @param {ol/Feature[]} features The features to prepare.
145
+ * @param {String} code The source EPSG projection code.
146
+ * @returns {ol/Feature[]} The prepared features.
147
+ */
148
+ function prepareFeatures (features, code) {
149
+ const downloadFeatures = [];
150
+
151
+ features.forEach((drawnFeature) => {
152
+ const feature = drawnFeature.clone(),
153
+ geometry = feature.getGeometry();
154
+
155
+ if (geometry instanceof Circle || geometry.getType() === "Circle") {
156
+ feature.setGeometry(fromCircle(geometry));
157
+ transformGeometry(code, geometry);
158
+ feature.set("masterportal_attributes", Object.assign(feature.get("masterportal_attributes") ?? {}, {
159
+ "isGeoCircle": true,
160
+ "geoCircleCenter": geometry.getCenter().join(","),
161
+ "geoCircleRadius": geometry.getRadius()
162
+ }));
163
+ }
164
+ downloadFeatures.push(feature);
165
+ });
166
+ return downloadFeatures;
167
+ }
168
+
169
+ /**
170
+ * Sets the geometry of each feature as WKT and the EPSG code to its csv_attributes.
171
+ *
172
+ * @param {ol/Feature[]} features An array of features.
173
+ * @param {String} code The EPSG code in which the features are coded.
174
+ * @returns {ol/Feature[]|Boolean} The features incl. the WKT geometries. False if the given parameter is not an array.
175
+ */
176
+ export function setCsvAttributes (features, code) {
177
+ if (!Array.isArray(features)) {
178
+ return false;
179
+ }
180
+ const wktParser = new WKT();
181
+
182
+ features.forEach(feature => {
183
+ if (feature instanceof Feature) {
184
+ let geom = feature.getGeometry();
185
+
186
+ if (geom instanceof Circle) {
187
+ geom = fromCircle(geom);
188
+ }
189
+ const wktGeometry = wktParser.writeGeometry(geom);
190
+
191
+ if (!isObject(feature.get("csv_attributes"))) {
192
+ feature.set("csv_attributes", {});
193
+ }
194
+ feature.get("csv_attributes").geometry = wktGeometry;
195
+ feature.get("csv_attributes").epsg = code;
196
+
197
+ Object.keys(feature.getProperties()).forEach(key => {
198
+ if (!["masterportal_attributes", "geometry", "csv_attributes"].includes(key)) {
199
+ feature.get("csv_attributes")[key] = feature.get(key);
200
+ }
201
+ });
202
+ }
203
+ });
204
+
205
+ return features;
206
+ }
207
+
208
+ export default {
209
+ convertFeatures,
210
+ transform,
211
+ transformPoint,
212
+ transformGeometry,
213
+ transformCoordinates,
214
+ prepareFeatures,
215
+ setCsvAttributes
216
+ };
@@ -0,0 +1,315 @@
1
+ /* eslint-disable no-underscore-dangle */
2
+ import {KML} from "ol/format.js";
3
+ import Feature from "ol/Feature.js";
4
+ import {isObject, transformPoint} from "./utils.js";
5
+
6
+
7
+ /**
8
+ * Converts an RGBA color array to a KML hex color string (aabbggrr format).
9
+ *
10
+ * @param {Number[]} rgba The color as [r, g, b, a] where r/g/b are 0-255 and a is 0-1.
11
+ * @returns {String} The KML hex color string in aabbggrr format, or empty string if input is invalid.
12
+ */
13
+ function rgbaToKmlColorHex (rgba) {
14
+ if (!Array.isArray(rgba) || rgba.length < 3) {
15
+ return "";
16
+ }
17
+ const r = Math.round(rgba[0]).toString(16).padStart(2, "0"),
18
+ g = Math.round(rgba[1]).toString(16).padStart(2, "0"),
19
+ b = Math.round(rgba[2]).toString(16).padStart(2, "0"),
20
+ a = rgba.length >= 4 ? Math.round(rgba[3] * 255).toString(16).padStart(2, "0") : "ff";
21
+
22
+ return `${a}${b}${g}${r}`;
23
+ }
24
+
25
+ /**
26
+ * Creates the IconStyle-Part of a Point-KML. Contains the link to a SVG.
27
+ *
28
+ * @see https://developers.google.com/kml/documentation/kmlreference#iconstyle
29
+ * @param {String} url URL from where the Icon can be retrieved from.
30
+ * @param {Number} scale Scale of the Icon. NOTE: If this value is 0, the Icon is not displayed.
31
+ * @param {String} [color] Optional KML hex color string (aabbggrr format).
32
+ * @returns {String} The IconStyle-Part of a KML-File.
33
+ */
34
+ function createKmlIconStyle (url, scale, color) {
35
+ const scaleTag = `<scale>${scale}</scale>`,
36
+ href = `<href>${url}</href>`,
37
+ colorTag = color ? `<color>${color}</color>` : "";
38
+
39
+ return `<IconStyle>${colorTag}${scaleTag}<Icon>${href}</Icon></IconStyle>`;
40
+ }
41
+
42
+ /**
43
+ * Constructs the hotspot-tag (anchoring of the icon) of an IconStyle-Part of a Point-KML.
44
+ *
45
+ * @see https://developers.google.com/kml/documentation/kmlreference#hotspot
46
+ * @param {Object} anchor Values for the hotspot-tag are retrieved from this object.
47
+ * @returns {String} hotspot-Tag for a KML IconStyle.
48
+ */
49
+ function getKmlHotSpotOfIconStyle (anchor) {
50
+ const x = anchor.anchor[0],
51
+ y = anchor.anchor[1],
52
+ {xUnit, yUnit} = anchor;
53
+
54
+ return `<hotSpot x="${x}" y="${y}" xunits="${xUnit}" yunits="${yUnit}" />`;
55
+ }
56
+
57
+ /**
58
+ * Transforms the given coordinates from EPSG:25832 to EPSG:4326, regardless of their nesting depth.
59
+ * Handles single coordinates as well as the nested arrays of (multi) lines and (multi) polygons.
60
+ *
61
+ * @param {(Array<number>|Array<Array<number>>|Array<Array<Array<number>>>)} coords Coordinates.
62
+ * @param {String} projectionCode Source projection code (e.g. "EPSG:25832").
63
+ * @returns {(Array<number>|Array<Array<number>>|Array<Array<Array<number>>>)} Transformed coordinates.
64
+ */
65
+ function transform (coords, projectionCode) {
66
+ if (!Array.isArray(coords) || coords.length === 0) {
67
+ return [];
68
+ }
69
+ if (Array.isArray(coords[0])) {
70
+ return coords.map(nested => transform(nested, projectionCode));
71
+ }
72
+ return transformPoint(projectionCode, coords);
73
+ }
74
+
75
+
76
+ /**
77
+ * Transforms the given geometry from EPSG:25832 to EPSG:4326.
78
+ * If the geometry type is not supported, an empty array is returned.
79
+ *
80
+ * @param {module:ol/geom/Geometry} geometry Geometry to be transformed.
81
+ * @param {String} projectionCode Source projection code (e.g. "EPSG:25832").
82
+ * @returns {(Array<number>|Array<Array<number>>|Array<Array<Array<number>>>)} The transformed coordinates or an empty array.
83
+ */
84
+ function transformCoordinates (geometry, projectionCode) {
85
+ const type = geometry.getType();
86
+
87
+ if (!["Point", "MultiPoint", "LineString", "MultiLineString", "Polygon", "MultiPolygon"].includes(type)) {
88
+ console.warn(`Geometry type "${type}" is not supported for transformation.`);
89
+ return [];
90
+ }
91
+
92
+ if (!projectionCode) {
93
+ return geometry.getCoordinates();
94
+ }
95
+
96
+ return transform(geometry.getCoordinates(), projectionCode);
97
+ }
98
+
99
+ /**
100
+ * Gets a KML document which has custom attributes from given features added.
101
+ *
102
+ * @param {ol/Feature[]} features The features.
103
+ * @param {module:ol/format/KML} format The KML format instance.
104
+ * @returns {Document|null} The KML Document or null if no attributes are present.
105
+ */
106
+ function getKMLWithCustomAttributes (features, format) {
107
+ if (!Array.isArray(features) || !isObject(format) || !features.some(feature => typeof feature.get === "function" && feature.get("attributes"))) {
108
+ return null;
109
+ }
110
+ const kml = new DOMParser().parseFromString(format.writeFeatures(features), "text/xml"),
111
+ placemarks = kml.getElementsByTagName("Placemark");
112
+
113
+ if (!placemarks.length) {
114
+ return null;
115
+ }
116
+ features.forEach((feature, idx) => {
117
+ const attributes = feature.get("attributes"),
118
+ attributeKeys = isObject(attributes) ? Object.keys(attributes) : [];
119
+
120
+ if (!attributeKeys.length) {
121
+ return;
122
+ }
123
+ attributeKeys.forEach(attrKey => {
124
+ if (placemarks[idx]) {
125
+ const extendedData = placemarks[idx].querySelector("ExtendedData");
126
+
127
+ if (!(extendedData instanceof Element)) {
128
+ return;
129
+ }
130
+
131
+ // KML native tags like name or description are not written into ExtendedData, so no Data node exists for them.
132
+ const data = extendedData.querySelector(`Data[name='${attrKey}']`),
133
+ existingDataNode = extendedData.querySelector(`Data[name='custom-attribute____${attrKey}']`);
134
+
135
+ if (existingDataNode instanceof Element) {
136
+ existingDataNode.remove();
137
+ }
138
+
139
+ if (data instanceof Element) {
140
+ data.setAttribute("name", `custom-attribute____${attrKey}`);
141
+ }
142
+ }
143
+ });
144
+ });
145
+ return kml;
146
+ }
147
+
148
+ /**
149
+ * Converts features to a KML string, transforming coordinates from EPSG:25832 to EPSG:4326.
150
+ *
151
+ * @param {ol/Feature[]} features The features to be converted.
152
+ * @param {module:ol/format/KML} format The KML format instance.
153
+ * @param {String} projectionCode Source projection code (e.g. "EPSG:25832").
154
+ * @returns {String} The features converted to a KML string.
155
+ */
156
+ function convertFeatures (features, format, projectionCode) {
157
+ const convertedFeatures = [];
158
+ let kml = null;
159
+
160
+ for (const feature of features) {
161
+ const cloned = feature.clone(),
162
+ transCoords = transformCoordinates(cloned.getGeometry(), projectionCode);
163
+
164
+ if (transCoords.length === 3 && transCoords[2] === 0) {
165
+ transCoords.pop();
166
+ }
167
+
168
+ cloned.getGeometry().setCoordinates(transCoords, "XY");
169
+ convertedFeatures.push(cloned);
170
+ }
171
+
172
+ kml = getKMLWithCustomAttributes(convertedFeatures, format);
173
+ if (kml === null) {
174
+ return format.writeFeatures(convertedFeatures);
175
+ }
176
+ return new XMLSerializer().serializeToString(kml);
177
+ }
178
+
179
+ /**
180
+ * Converts the features to KML while also saving its style information.
181
+ * @param {ol.Feature[]} features - the used features
182
+ * @param {String} projectionCode Source projection code (e.g. "EPSG:25832").
183
+ * @returns {String} The features written in KML as a String.
184
+ */
185
+ async function convertFeaturesToKml (features, projectionCode) {
186
+ const featureCount = features.length,
187
+ anchors = Array(featureCount).fill(undefined),
188
+ format = new KML({extractStyles: true}),
189
+ hasIconUrl = Array(featureCount).fill(false),
190
+ pointColors = Array(featureCount).fill(undefined),
191
+ pointScales = Array(featureCount).fill(undefined),
192
+ skip = Array(featureCount).fill(false),
193
+ textFonts = Array(featureCount).fill(undefined),
194
+ textFontSize = Array(featureCount).fill(undefined),
195
+ convertedFeatures = new DOMParser().parseFromString(convertFeatures(features, format, projectionCode), "text/xml");
196
+
197
+ features.forEach((feature, i) => {
198
+ const type = feature.getGeometry().getType();
199
+ let color,
200
+ style,
201
+ styles;
202
+
203
+ if (type === "Point" && feature.values_.drawState && feature.values_.drawState.text !== undefined) {
204
+ // Imported KML with text, can be used as it is
205
+ skip[i] = true;
206
+ textFontSize[i] = feature.values_.drawState.fontSize;
207
+ }
208
+ else {
209
+ try {
210
+ styles = feature.getStyleFunction()(feature);
211
+ style = Array.isArray(styles) ? styles[0] : styles;
212
+ }
213
+ catch (err) {
214
+ // Only happens if an imported KML is exported, can be skipped
215
+ skip[i] = true;
216
+ }
217
+
218
+ if (type === "Point") {
219
+ if (style.getImage() !== null && style.getImage().iconImage_ !== undefined) {
220
+ // Imported KML with link to SVG icon, has iconUrl from previous import
221
+ hasIconUrl[i] = true;
222
+ const anchorXUnits = style.getImage().anchorXUnits_,
223
+ anchorYUnits = style.getImage().anchorYUnits_,
224
+ anchor = style.getImage().anchor_;
225
+
226
+ anchors[i] = {xUnit: anchorXUnits, yUnit: anchorYUnits, anchor: anchor};
227
+ }
228
+ else if (style.getText()) {
229
+ textFonts[i] = style.getText().getFont();
230
+ }
231
+ else {
232
+ color = style.getImage().getFill().getColor();
233
+ pointColors[i] = [color[0], color[1], color[2]];
234
+ pointScales[i] = Math.max(...style.getImage().getSize()) / 32;
235
+ }
236
+ }
237
+ }
238
+ });
239
+
240
+ Array.from(convertedFeatures.getElementsByTagName("Placemark")).forEach((placemark, i) => {
241
+ if (placemark.getElementsByTagName("Point").length > 0 && skip[i] === false) {
242
+ const style = placemark.getElementsByTagName("Style")[0];
243
+
244
+ if (hasIconUrl[i] === false && pointColors[i]) {
245
+ const drawStateColor = features[i].values_ && features[i].values_.drawState ? features[i].values_.drawState.color : undefined,
246
+ kmlColor = drawStateColor ? rgbaToKmlColorHex(drawStateColor) : "",
247
+ iconUrl = `${window.location.origin}/src/assets/img/tools/draw/circle_white.svg`,
248
+ iconStyle = createKmlIconStyle(iconUrl, pointScales[i], kmlColor);
249
+
250
+ style.innerHTML += iconStyle;
251
+ }
252
+ else if (hasIconUrl[i] === true && anchors[i] !== undefined) {
253
+ const iconStyle = placemark.getElementsByTagName("IconStyle")[0];
254
+
255
+ iconStyle.innerHTML += getKmlHotSpotOfIconStyle(anchors[i]);
256
+ }
257
+ }
258
+
259
+ // Setting format for text
260
+ if (placemark.getElementsByTagName("Point").length > 0 && skip[i] === true && !isNaN(textFontSize[i])) {
261
+ const scale = textFontSize[i] / 16,
262
+ style = placemark.getElementsByTagName("Style")[0],
263
+ iconUrl = `${window.location.origin}/src/assets/img/tools/draw/circle_blue.svg`,
264
+ maskIcon = new DOMParser().parseFromString("<IconStyle><scale>0</scale><Icon><href>" + iconUrl + "</href></Icon></IconStyle>", "text/xml"),
265
+ maskScale = new DOMParser().parseFromString("<scale>" + scale + "</scale>", "text/xml");
266
+
267
+ style.getElementsByTagName("LabelStyle")[0].appendChild(maskScale.getElementsByTagName("scale")[0]);
268
+ style.appendChild(maskIcon.getElementsByTagName("IconStyle")[0]);
269
+ }
270
+ });
271
+ return new XMLSerializer().serializeToString(convertedFeatures);
272
+ }
273
+
274
+ /**
275
+ * Sets the attributes of each feature as requested by the converting function.
276
+ *
277
+ * @param {ol/Feature[]} features An array of features.
278
+ * @returns {ol/Feature[]|Boolean} The features incl. the necessary attributes. False if the given parameter is not an array.
279
+ */
280
+ function setKmlAttributes (features) {
281
+ if (!Array.isArray(features)) {
282
+ return false;
283
+ }
284
+
285
+ features.forEach(feature => {
286
+ if (feature instanceof Feature && !Object.prototype.hasOwnProperty.call(feature.getProperties(), "attributes")) {
287
+ feature.set("attributes", {});
288
+
289
+ Object.keys(feature.getProperties()).forEach(key => {
290
+ if (!["masterportal_attributes", "geometry", "attributes"].includes(key)) {
291
+ feature.get("attributes")[key] = feature.get(key);
292
+ }
293
+ });
294
+
295
+ if (Object.prototype.hasOwnProperty.call(feature.getProperties(), "masterportal_attributes")) {
296
+ Object.keys(feature.get("masterportal_attributes")).forEach(key => {
297
+ const value = feature.get("masterportal_attributes")[key];
298
+
299
+ feature.set(key, key === "drawState" && isObject(value) ? JSON.stringify(value) : value);
300
+ });
301
+ }
302
+ }
303
+ });
304
+
305
+ return features;
306
+ }
307
+
308
+
309
+ export default {
310
+ convertFeatures,
311
+ convertFeaturesToKml,
312
+ transformCoordinates,
313
+ getKMLWithCustomAttributes,
314
+ setKmlAttributes
315
+ };
@@ -0,0 +1,60 @@
1
+ import proj4 from "proj4";
2
+
3
+ /**
4
+ * Checks if the passed parameter is an object.
5
+ *
6
+ * @param {*} value parameter to check.
7
+ * @returns {Boolean} true if the value is an object; false otherwise.
8
+ */
9
+ function isObject (value) {
10
+ return Object.prototype.toString.call(value) === "[object Object]";
11
+ }
12
+
13
+ /**
14
+ * Registers a source projection with proj4 and returns both source and destination projection objects.
15
+ *
16
+ * @param {String} sourceProj Source projection name (e.g. "EPSG:25832").
17
+ * @param {String} destProj Destination projection name (e.g. "EPSG:4326").
18
+ * @param {String} zone UTM zone of the source projection.
19
+ * @returns {Object} An object with proj4 projection instances for sourceProj and destProj.
20
+ */
21
+ function getProjections (sourceProj, destProj, zone) {
22
+ proj4.defs(sourceProj, "+proj=utm +zone=" + zone + " +ellps=WGS84 +towgs84=0,0,0,0,0,0,1 +units=m +no_defs");
23
+
24
+ return {
25
+ sourceProj: proj4(sourceProj),
26
+ destProj: proj4(destProj)
27
+ };
28
+ }
29
+
30
+ const projections = getProjections("EPSG:25832", "EPSG:4326", "32");
31
+
32
+ /**
33
+ * Transforms the given point coordinates from a given source projection to EPSG:4326.
34
+ *
35
+ * @param {String} sourceProjectionCode Source projection code (e.g. "EPSG:25832").
36
+ * @param {Number[]} coords Coordinates to transform.
37
+ * @returns {Number[]} Transformed coordinates in EPSG:4326.
38
+ */
39
+ function transformPoint (sourceProjectionCode, coords) {
40
+ return proj4(proj4(sourceProjectionCode), proj4("EPSG:4326"), coords);
41
+ }
42
+
43
+ /**
44
+ * Transforms the given geometry in-place from a given source projection to EPSG:4326.
45
+ *
46
+ * @param {String} sourceProjectionCode Source projection code (e.g. "EPSG:25832").
47
+ * @param {module:ol/geom/Geometry} geometry Geometry to transform.
48
+ * @returns {module:ol/geom/Geometry} The transformed geometry.
49
+ */
50
+ function transformGeometry (sourceProjectionCode, geometry) {
51
+ return geometry.transform(sourceProjectionCode, "EPSG:4326");
52
+ }
53
+
54
+ export {
55
+ isObject,
56
+ getProjections,
57
+ projections,
58
+ transformGeometry,
59
+ transformPoint
60
+ };
@@ -9,6 +9,35 @@ import {getRuleForIndex, getRulesForFeature} from "./lib/getRuleForIndex";
9
9
 
10
10
  const legendsOfAllStyles = [];
11
11
  let legendInformationLength = 0;
12
+ const warnedLayerIdsWithDefaultStyleFallback = new Set();
13
+
14
+ /**
15
+ * Warns once per layer when no styling rule matches and default styles are used.
16
+ * @param {object} [warningContext={}] context for the warning
17
+ * @param {string} [warningContext.layerId] id of the affected layer
18
+ * @returns {void}
19
+ */
20
+ function warnAboutDefaultStyleFallback (warningContext = {}) {
21
+ const {layerId} = warningContext;
22
+
23
+ if (typeof layerId === "string" && layerId.length > 0) {
24
+ if (warnedLayerIdsWithDefaultStyleFallback.has(layerId)) {
25
+ return;
26
+ }
27
+ warnedLayerIdsWithDefaultStyleFallback.add(layerId);
28
+ console.warn(`No valid styling rule found for layer "${layerId}". Falling back to defaults.`);
29
+ return;
30
+ }
31
+ console.warn("No valid styling rule found. Falling back to defaults");
32
+ }
33
+
34
+ /**
35
+ * Clears cached default-style fallback warnings.
36
+ * @returns {void}
37
+ */
38
+ function clearDefaultStyleFallbackWarnings () {
39
+ warnedLayerIdsWithDefaultStyleFallback.clear();
40
+ }
12
41
 
13
42
  /**
14
43
  * Returns true if feature contains some kind of MultiGeometry
@@ -152,9 +181,10 @@ function getMultiGeometryStyle (geometryType, feature, rules, isClustered, wfsIm
152
181
  * @param {object[]} rules styling rules to check. Array can be empty.
153
182
  * @param {Boolean} isClustered Flag to show if feature is clustered.
154
183
  * @param {String} wfsImgPathFromConfig path to wfsImg from Config
184
+ * @param {object} [warningContext={}] context for warning deduplication
155
185
  * @returns {ol/style/Style} style is always returned
156
186
  */
157
- function getGeometryStyle (feature, rules, isClustered, wfsImgPathFromConfig) {
187
+ function getGeometryStyle (feature, rules, isClustered, wfsImgPathFromConfig, warningContext = {}) {
158
188
  const geometryType = feature ? feature.getGeometry().getType() : "cesium";
159
189
 
160
190
  // For simple geometries the first styling rule is used.
@@ -168,7 +198,7 @@ function getGeometryStyle (feature, rules, isClustered, wfsImgPathFromConfig) {
168
198
  }
169
199
 
170
200
  // fall back to default styles as configured in geomType specific styles, if no rule is matched
171
- console.warn("No valid styling rule found. Falling back to defaults");
201
+ warnAboutDefaultStyleFallback(warningContext);
172
202
  return isMultiGeometry(geometryType)
173
203
  ? getMultiGeometryStyle(geometryType, feature, undefined, isClustered, wfsImgPathFromConfig)
174
204
  : getSimpleGeometryStyle(geometryType, feature, undefined, isClustered, wfsImgPathFromConfig);
@@ -224,14 +254,15 @@ function captureLegendFromFeature (styleId, legendInformation) {
224
254
  * @param {ol/feature} feature the feature to style
225
255
  * @param {Boolean} isClustered is feature clustered
226
256
  * @param {String} wfsImgPathFromConfig path to wfsImg from Config
227
- * @returns {ol/style/Style} style used in layer model
257
+ * @param {object} [warningContext={}] context for warning deduplication
258
+ * @returns {ol/style/Style} style used in layer model
228
259
  */
229
- function createStyle (styleObject, feature, isClustered, wfsImgPathFromConfig) {
260
+ function createStyle (styleObject, feature, isClustered, wfsImgPathFromConfig, warningContext = {}) {
230
261
  const rules = getRulesForFeature(styleObject, feature),
231
262
  // Takes first rule in array for labeling so that is giving precedence to the order in the style.json
232
263
  style = Array.isArray(rules) && rules.length > 0 ? rules[0].style : null,
233
264
  hasLabelField = style?.labelField,
234
- geometryStyle = getGeometryStyle(feature, rules, isClustered, wfsImgPathFromConfig),
265
+ geometryStyle = getGeometryStyle(feature, rules, isClustered, wfsImgPathFromConfig, warningContext),
235
266
  legendInformation = Array.isArray(geometryStyle) ? geometryStyle[0].legendInfos : geometryStyle.legendInfos;
236
267
  let styleObjectGeometry = null;
237
268
 
@@ -327,5 +358,6 @@ export default {
327
358
  getGeometryStyle,
328
359
  getLabelStyle,
329
360
  createStyle,
361
+ clearDefaultStyleFallbackWarnings,
330
362
  returnLegendByStyleId
331
363
  };