@eeacms/volto-arcgis-block 0.1.427 → 0.1.429
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
|
@@ -4,6 +4,19 @@ All notable changes to this project will be documented in this file. Dates are d
|
|
|
4
4
|
|
|
5
5
|
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
6
6
|
|
|
7
|
+
### [0.1.429](https://github.com/eea/volto-arcgis-block/compare/0.1.428...0.1.429) - 17 February 2026
|
|
8
|
+
|
|
9
|
+
#### :hammer_and_wrench: Others
|
|
10
|
+
|
|
11
|
+
- (bug): styles for buttons in upload widget and error popup message for file size limit on hspae files [Unai Bolivar - [`d9ca59e`](https://github.com/eea/volto-arcgis-block/commit/d9ca59ec245fdc94a2d1bf678b6e3d610d5a792d)]
|
|
12
|
+
- (bug): reset upload widget to allow users to start from zero without finishing a process first [Unai Bolivar - [`1539a3a`](https://github.com/eea/volto-arcgis-block/commit/1539a3ada8fef0fbda5203b61ec2aed60ab014b5)]
|
|
13
|
+
- (bug): truncate title to 33 max char and add ellipsis for user created services and move trashcan icon to the right edge of the menu item div [Unai Bolivar - [`a31612d`](https://github.com/eea/volto-arcgis-block/commit/a31612df1ee629937e115b1fec1c50092b659626)]
|
|
14
|
+
- (bug): fix xml to json conversion for unsupported features and error report for features with no geometry data [Unai Bolivar - [`51774c1`](https://github.com/eea/volto-arcgis-block/commit/51774c155b59c8a0624d725ffb12cf130b1f65f1)]
|
|
15
|
+
- (bug): Check for match between selected service and provided service url [Unai Bolivar - [`d7247dc`](https://github.com/eea/volto-arcgis-block/commit/d7247dc29755df4e70c6b4ad3b175f0e1e29f24b)]
|
|
16
|
+
- (bug): Extent button in active layers bug fixed for user created services [Unai Bolivar - [`dfee085`](https://github.com/eea/volto-arcgis-block/commit/dfee085a821b9a85e21dd48ab3c7a0988d135b77)]
|
|
17
|
+
- (bug): fix trash can icon styles for user created services [Unai Bolivar - [`0588462`](https://github.com/eea/volto-arcgis-block/commit/05884627281671b9186608c65570e7fd95cfe999)]
|
|
18
|
+
### [0.1.428](https://github.com/eea/volto-arcgis-block/compare/0.1.427...0.1.428) - 13 February 2026
|
|
19
|
+
|
|
7
20
|
### [0.1.427](https://github.com/eea/volto-arcgis-block/compare/0.1.426...0.1.427) - 11 February 2026
|
|
8
21
|
|
|
9
22
|
### [0.1.426](https://github.com/eea/volto-arcgis-block/compare/0.1.425...0.1.426) - 9 February 2026
|
package/package.json
CHANGED
|
@@ -344,6 +344,7 @@ class MapViewer extends React.Component {
|
|
|
344
344
|
userServiceType: '',
|
|
345
345
|
userServiceFile: null,
|
|
346
346
|
uploadError: false,
|
|
347
|
+
uploadErrorType: 'uploadError',
|
|
347
348
|
// Track current user state for comparison in componentDidUpdate
|
|
348
349
|
currentUserState: props.initialUserState || {
|
|
349
350
|
user_id: null,
|
|
@@ -613,10 +614,10 @@ class MapViewer extends React.Component {
|
|
|
613
614
|
this.setState({ uploadedFile: message });
|
|
614
615
|
}
|
|
615
616
|
|
|
616
|
-
uploadFileErrorHandler = () => {
|
|
617
|
-
this.setState({ uploadError: true });
|
|
617
|
+
uploadFileErrorHandler = (errorType = 'uploadError') => {
|
|
618
|
+
this.setState({ uploadError: true, uploadErrorType: errorType });
|
|
618
619
|
setTimeout(() => {
|
|
619
|
-
this.setState({ uploadError: false });
|
|
620
|
+
this.setState({ uploadError: false, uploadErrorType: 'uploadError' });
|
|
620
621
|
}, 3000);
|
|
621
622
|
};
|
|
622
623
|
|
|
@@ -1036,6 +1037,7 @@ class MapViewer extends React.Component {
|
|
|
1036
1037
|
mapViewer={this}
|
|
1037
1038
|
wmsServiceUrl={this.state.wmsServiceUrl}
|
|
1038
1039
|
showErrorPopup={this.state.uploadError}
|
|
1040
|
+
showErrorPopupType={this.state.uploadErrorType}
|
|
1039
1041
|
uploadUrlServiceHandler={this.uploadUrlServiceHandler}
|
|
1040
1042
|
uploadFileErrorHandler={this.uploadFileErrorHandler}
|
|
1041
1043
|
/>
|
|
@@ -2421,12 +2421,547 @@ class MenuWidget extends React.Component {
|
|
|
2421
2421
|
}
|
|
2422
2422
|
}
|
|
2423
2423
|
|
|
2424
|
+
resolveRequestConfig(xml) {
|
|
2425
|
+
let doc = xml;
|
|
2426
|
+
try {
|
|
2427
|
+
if (typeof xml === 'string') {
|
|
2428
|
+
const parser = new DOMParser();
|
|
2429
|
+
doc = parser.parseFromString(xml, 'text/xml');
|
|
2430
|
+
}
|
|
2431
|
+
const versionSet = new Set();
|
|
2432
|
+
const formatSet = new Set();
|
|
2433
|
+
const rootVersion = doc?.documentElement?.getAttribute?.('version');
|
|
2434
|
+
if (rootVersion) {
|
|
2435
|
+
versionSet.add(rootVersion.trim());
|
|
2436
|
+
}
|
|
2437
|
+
const serviceTypeVersionNodes = doc.querySelectorAll(
|
|
2438
|
+
'ServiceTypeVersion, ows\\:ServiceTypeVersion',
|
|
2439
|
+
);
|
|
2440
|
+
serviceTypeVersionNodes.forEach((node) => {
|
|
2441
|
+
const nodeValue = (node.textContent || '').trim();
|
|
2442
|
+
if (nodeValue) {
|
|
2443
|
+
versionSet.add(nodeValue);
|
|
2444
|
+
}
|
|
2445
|
+
});
|
|
2446
|
+
const acceptVersionNodes = doc.querySelectorAll(
|
|
2447
|
+
'Parameter[name="AcceptVersions"] Value, ows\\:Parameter[name="AcceptVersions"] ows\\:Value',
|
|
2448
|
+
);
|
|
2449
|
+
acceptVersionNodes.forEach((node) => {
|
|
2450
|
+
const nodeValue = (node.textContent || '').trim();
|
|
2451
|
+
if (nodeValue) {
|
|
2452
|
+
versionSet.add(nodeValue);
|
|
2453
|
+
}
|
|
2454
|
+
});
|
|
2455
|
+
const outputNodes = doc.querySelectorAll(
|
|
2456
|
+
'OutputFormats > Format, OutputFormat, Parameter[name="outputFormat"] Value, ows\\:Parameter[name="outputFormat"] ows\\:Value',
|
|
2457
|
+
);
|
|
2458
|
+
outputNodes.forEach((node) => {
|
|
2459
|
+
const nodeValue = (node.textContent || '').trim();
|
|
2460
|
+
if (nodeValue) {
|
|
2461
|
+
formatSet.add(nodeValue);
|
|
2462
|
+
}
|
|
2463
|
+
});
|
|
2464
|
+
const srsSet = new Set();
|
|
2465
|
+
const srsNodes = doc.querySelectorAll(
|
|
2466
|
+
'DefaultSRS, DefaultCRS, OtherSRS, OtherCRS, Parameter[name="srsName"] Value, ows\\:Parameter[name="srsName"] ows\\:Value',
|
|
2467
|
+
);
|
|
2468
|
+
srsNodes.forEach((node) => {
|
|
2469
|
+
const nodeValue = (node.textContent || '').trim();
|
|
2470
|
+
if (nodeValue) {
|
|
2471
|
+
srsSet.add(nodeValue);
|
|
2472
|
+
}
|
|
2473
|
+
});
|
|
2474
|
+
const versionList = Array.from(versionSet);
|
|
2475
|
+
const formatList = Array.from(formatSet);
|
|
2476
|
+
const srsList = Array.from(srsSet);
|
|
2477
|
+
const requestVersion = versionList.includes('2.0.0')
|
|
2478
|
+
? '2.0.0'
|
|
2479
|
+
: versionList.includes('1.1.0')
|
|
2480
|
+
? '1.1.0'
|
|
2481
|
+
: versionList.includes('1.0.0')
|
|
2482
|
+
? '1.0.0'
|
|
2483
|
+
: rootVersion || '2.0.0';
|
|
2484
|
+
const preferredJsonFormat = formatList.find((item) =>
|
|
2485
|
+
/application\/json|geojson|json/i.test(item),
|
|
2486
|
+
);
|
|
2487
|
+
const preferredXmlFormat = formatList.find((item) =>
|
|
2488
|
+
/text\/xml|xml|gml/i.test(item),
|
|
2489
|
+
);
|
|
2490
|
+
const requestSrsName = srsList.includes('EPSG:4326')
|
|
2491
|
+
? 'EPSG:4326'
|
|
2492
|
+
: srsList[0] || 'EPSG:4326';
|
|
2493
|
+
return {
|
|
2494
|
+
requestVersion,
|
|
2495
|
+
requestFormat: preferredJsonFormat || preferredXmlFormat || null,
|
|
2496
|
+
hasJsonFormat: Boolean(preferredJsonFormat),
|
|
2497
|
+
requestSrsName,
|
|
2498
|
+
};
|
|
2499
|
+
} catch (e) {
|
|
2500
|
+
return {
|
|
2501
|
+
requestVersion: '2.0.0',
|
|
2502
|
+
requestFormat: 'application/json',
|
|
2503
|
+
hasJsonFormat: true,
|
|
2504
|
+
requestSrsName: 'EPSG:4326',
|
|
2505
|
+
};
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
resolveExceptionMessage(responseText) {
|
|
2510
|
+
const textValue = typeof responseText === 'string' ? responseText : '';
|
|
2511
|
+
if (!textValue) {
|
|
2512
|
+
return '';
|
|
2513
|
+
}
|
|
2514
|
+
const exceptionMatch = textValue.match(
|
|
2515
|
+
/<(?:\w+:)?ExceptionText>\s*([\s\S]*?)\s*<\/(?:\w+:)?ExceptionText>/i,
|
|
2516
|
+
);
|
|
2517
|
+
if (exceptionMatch && exceptionMatch[1]) {
|
|
2518
|
+
return exceptionMatch[1].trim();
|
|
2519
|
+
}
|
|
2520
|
+
return '';
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
isNoGeometryError(error) {
|
|
2524
|
+
const message = ((error && error.message) || '').toLowerCase();
|
|
2525
|
+
return (
|
|
2526
|
+
message.includes('no geometry') ||
|
|
2527
|
+
message.includes('cannot be displayed on map')
|
|
2528
|
+
);
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
resolveUploadErrorType(error) {
|
|
2532
|
+
const detailsFromRoot =
|
|
2533
|
+
error && error.details && Array.isArray(error.details)
|
|
2534
|
+
? error.details
|
|
2535
|
+
: [];
|
|
2536
|
+
const detailsFromNested =
|
|
2537
|
+
error &&
|
|
2538
|
+
error.details &&
|
|
2539
|
+
error.details.details &&
|
|
2540
|
+
Array.isArray(error.details.details)
|
|
2541
|
+
? error.details.details
|
|
2542
|
+
: [];
|
|
2543
|
+
const detailsFromResponse =
|
|
2544
|
+
error &&
|
|
2545
|
+
error.response &&
|
|
2546
|
+
error.response.data &&
|
|
2547
|
+
error.response.data.error &&
|
|
2548
|
+
Array.isArray(error.response.data.error.details)
|
|
2549
|
+
? error.response.data.error.details
|
|
2550
|
+
: [];
|
|
2551
|
+
const errorDetails = [
|
|
2552
|
+
...detailsFromRoot,
|
|
2553
|
+
...detailsFromNested,
|
|
2554
|
+
...detailsFromResponse,
|
|
2555
|
+
];
|
|
2556
|
+
const normalizedDetails = errorDetails.map((detailValue) =>
|
|
2557
|
+
String(detailValue || '').toLowerCase(),
|
|
2558
|
+
);
|
|
2559
|
+
const hasShapefileLimit = normalizedDetails.some((detailValue) =>
|
|
2560
|
+
detailValue.includes('shape file exceeds the max size allowed of 2mb'),
|
|
2561
|
+
);
|
|
2562
|
+
if (hasShapefileLimit) {
|
|
2563
|
+
return 'shapefileLimit';
|
|
2564
|
+
}
|
|
2565
|
+
const hasFileLimit = normalizedDetails.some((detailValue) =>
|
|
2566
|
+
detailValue.includes('file exceeds the max size allowed of 10mb'),
|
|
2567
|
+
);
|
|
2568
|
+
if (hasFileLimit) {
|
|
2569
|
+
return 'fileLimit';
|
|
2570
|
+
}
|
|
2571
|
+
return 'uploadError';
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
hasExceptionResponse(responseText) {
|
|
2575
|
+
const textValue = typeof responseText === 'string' ? responseText : '';
|
|
2576
|
+
if (!textValue) {
|
|
2577
|
+
return false;
|
|
2578
|
+
}
|
|
2579
|
+
return /<\s*(?:\w+:)?ExceptionReport\b|<\s*(?:\w+:)?Exception\b/i.test(
|
|
2580
|
+
textValue,
|
|
2581
|
+
);
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
processXmlData(xmlInput) {
|
|
2585
|
+
let doc = xmlInput;
|
|
2586
|
+
if (typeof xmlInput === 'string') {
|
|
2587
|
+
const parser = new DOMParser();
|
|
2588
|
+
doc = parser.parseFromString(xmlInput, 'text/xml');
|
|
2589
|
+
}
|
|
2590
|
+
if (!doc || !doc.documentElement) {
|
|
2591
|
+
return null;
|
|
2592
|
+
}
|
|
2593
|
+
const processNodeData = (node) => {
|
|
2594
|
+
const nodeData = {
|
|
2595
|
+
name: node.localName || node.nodeName,
|
|
2596
|
+
value: null,
|
|
2597
|
+
attributes: {},
|
|
2598
|
+
children: [],
|
|
2599
|
+
};
|
|
2600
|
+
if (node.attributes && node.attributes.length) {
|
|
2601
|
+
Array.from(node.attributes).forEach((attributeNode) => {
|
|
2602
|
+
nodeData.attributes[attributeNode.name] = attributeNode.value;
|
|
2603
|
+
});
|
|
2604
|
+
}
|
|
2605
|
+
if (node.childNodes && node.childNodes.length) {
|
|
2606
|
+
Array.from(node.childNodes).forEach((childNode) => {
|
|
2607
|
+
if (childNode.nodeType === 1) {
|
|
2608
|
+
nodeData.children.push(processNodeData(childNode));
|
|
2609
|
+
} else if (childNode.nodeType === 3) {
|
|
2610
|
+
const value = (childNode.nodeValue || '').trim();
|
|
2611
|
+
if (value) {
|
|
2612
|
+
nodeData.value = value;
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
});
|
|
2616
|
+
}
|
|
2617
|
+
return nodeData;
|
|
2618
|
+
};
|
|
2619
|
+
return processNodeData(doc.documentElement);
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
resolveNodeList(nodeData, nodeName) {
|
|
2623
|
+
const resultList = [];
|
|
2624
|
+
const processNodeData = (currentNode) => {
|
|
2625
|
+
if (!currentNode) {
|
|
2626
|
+
return;
|
|
2627
|
+
}
|
|
2628
|
+
if ((currentNode.name || '').toLowerCase() === nodeName.toLowerCase()) {
|
|
2629
|
+
resultList.push(currentNode);
|
|
2630
|
+
}
|
|
2631
|
+
(currentNode.children || []).forEach((childNode) => {
|
|
2632
|
+
processNodeData(childNode);
|
|
2633
|
+
});
|
|
2634
|
+
};
|
|
2635
|
+
processNodeData(nodeData);
|
|
2636
|
+
return resultList;
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
resolveCoordinateResult(valueText) {
|
|
2640
|
+
const textValue = (valueText || '').trim();
|
|
2641
|
+
if (!textValue) {
|
|
2642
|
+
return [];
|
|
2643
|
+
}
|
|
2644
|
+
if (textValue.includes(',')) {
|
|
2645
|
+
return textValue
|
|
2646
|
+
.trim()
|
|
2647
|
+
.split(/\s+/)
|
|
2648
|
+
.map((item) => item.split(',').map(Number))
|
|
2649
|
+
.filter((item) => item.length >= 2 && !item.some(Number.isNaN))
|
|
2650
|
+
.map((item) => [item[0], item[1]]);
|
|
2651
|
+
}
|
|
2652
|
+
const valueList = textValue
|
|
2653
|
+
.split(/\s+/)
|
|
2654
|
+
.map(Number)
|
|
2655
|
+
.filter((item) => !Number.isNaN(item));
|
|
2656
|
+
const resultList = [];
|
|
2657
|
+
for (let i = 0; i + 1 < valueList.length; i += 2) {
|
|
2658
|
+
resultList.push([valueList[i], valueList[i + 1]]);
|
|
2659
|
+
}
|
|
2660
|
+
return resultList;
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
resolveGeometryResult(featureData) {
|
|
2664
|
+
const geometryTypeList = [
|
|
2665
|
+
'Point',
|
|
2666
|
+
'LineString',
|
|
2667
|
+
'Polygon',
|
|
2668
|
+
'MultiPoint',
|
|
2669
|
+
'MultiLineString',
|
|
2670
|
+
'MultiPolygon',
|
|
2671
|
+
];
|
|
2672
|
+
const geometryNode = geometryTypeList
|
|
2673
|
+
.map((typeValue) => this.resolveNodeList(featureData, typeValue)[0])
|
|
2674
|
+
.find(Boolean);
|
|
2675
|
+
if (!geometryNode) {
|
|
2676
|
+
return null;
|
|
2677
|
+
}
|
|
2678
|
+
const nodeName = geometryNode.name;
|
|
2679
|
+
if (nodeName === 'Point') {
|
|
2680
|
+
const posNode =
|
|
2681
|
+
this.resolveNodeList(geometryNode, 'pos')[0] ||
|
|
2682
|
+
this.resolveNodeList(geometryNode, 'coordinates')[0];
|
|
2683
|
+
const points = this.resolveCoordinateResult(posNode?.value || '');
|
|
2684
|
+
if (!points.length) {
|
|
2685
|
+
return null;
|
|
2686
|
+
}
|
|
2687
|
+
return { type: 'Point', coordinates: points[0] };
|
|
2688
|
+
}
|
|
2689
|
+
if (nodeName === 'LineString') {
|
|
2690
|
+
const posListNode =
|
|
2691
|
+
this.resolveNodeList(geometryNode, 'posList')[0] ||
|
|
2692
|
+
this.resolveNodeList(geometryNode, 'coordinates')[0];
|
|
2693
|
+
const points = this.resolveCoordinateResult(posListNode?.value || '');
|
|
2694
|
+
if (!points.length) {
|
|
2695
|
+
return null;
|
|
2696
|
+
}
|
|
2697
|
+
return { type: 'LineString', coordinates: points };
|
|
2698
|
+
}
|
|
2699
|
+
if (nodeName === 'Polygon') {
|
|
2700
|
+
const ringNodes = this.resolveNodeList(geometryNode, 'LinearRing');
|
|
2701
|
+
const ringList = ringNodes
|
|
2702
|
+
.map((ringNode) => {
|
|
2703
|
+
const posListNode =
|
|
2704
|
+
this.resolveNodeList(ringNode, 'posList')[0] ||
|
|
2705
|
+
this.resolveNodeList(ringNode, 'coordinates')[0];
|
|
2706
|
+
return this.resolveCoordinateResult(posListNode?.value || '');
|
|
2707
|
+
})
|
|
2708
|
+
.filter((ringValue) => ringValue.length > 0);
|
|
2709
|
+
if (!ringList.length) {
|
|
2710
|
+
return null;
|
|
2711
|
+
}
|
|
2712
|
+
return { type: 'Polygon', coordinates: ringList };
|
|
2713
|
+
}
|
|
2714
|
+
return null;
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
resolveFeatureResult(featureData) {
|
|
2718
|
+
const geometryResult = this.resolveGeometryResult(featureData);
|
|
2719
|
+
if (!geometryResult) {
|
|
2720
|
+
return null;
|
|
2721
|
+
}
|
|
2722
|
+
const processPropertyData = (nodeData, propertyData) => {
|
|
2723
|
+
if (!nodeData) {
|
|
2724
|
+
return;
|
|
2725
|
+
}
|
|
2726
|
+
const geometryNames = new Set([
|
|
2727
|
+
'Point',
|
|
2728
|
+
'LineString',
|
|
2729
|
+
'Polygon',
|
|
2730
|
+
'MultiPoint',
|
|
2731
|
+
'MultiLineString',
|
|
2732
|
+
'MultiPolygon',
|
|
2733
|
+
'pos',
|
|
2734
|
+
'posList',
|
|
2735
|
+
'coordinates',
|
|
2736
|
+
'LinearRing',
|
|
2737
|
+
'exterior',
|
|
2738
|
+
'interior',
|
|
2739
|
+
'boundedBy',
|
|
2740
|
+
]);
|
|
2741
|
+
if (geometryNames.has(nodeData.name)) {
|
|
2742
|
+
return;
|
|
2743
|
+
}
|
|
2744
|
+
if ((nodeData.children || []).length === 0 && nodeData.value) {
|
|
2745
|
+
if (!propertyData[nodeData.name]) {
|
|
2746
|
+
propertyData[nodeData.name] = nodeData.value;
|
|
2747
|
+
}
|
|
2748
|
+
return;
|
|
2749
|
+
}
|
|
2750
|
+
(nodeData.children || []).forEach((childNode) => {
|
|
2751
|
+
processPropertyData(childNode, propertyData);
|
|
2752
|
+
});
|
|
2753
|
+
};
|
|
2754
|
+
const properties = {};
|
|
2755
|
+
processPropertyData(featureData, properties);
|
|
2756
|
+
const featureId =
|
|
2757
|
+
featureData.attributes['gml:id'] ||
|
|
2758
|
+
featureData.attributes.id ||
|
|
2759
|
+
featureData.attributes.fid ||
|
|
2760
|
+
null;
|
|
2761
|
+
return {
|
|
2762
|
+
type: 'Feature',
|
|
2763
|
+
id: featureId,
|
|
2764
|
+
geometry: geometryResult,
|
|
2765
|
+
properties,
|
|
2766
|
+
};
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2769
|
+
resolveFeatureCollection(xmlData) {
|
|
2770
|
+
const memberList = [
|
|
2771
|
+
...this.resolveNodeList(xmlData, 'featureMember'),
|
|
2772
|
+
...this.resolveNodeList(xmlData, 'member'),
|
|
2773
|
+
];
|
|
2774
|
+
const featureList = memberList
|
|
2775
|
+
.map((memberData) => {
|
|
2776
|
+
const featureData = (memberData.children || []).find(
|
|
2777
|
+
(childData) => childData.name !== 'boundedBy',
|
|
2778
|
+
);
|
|
2779
|
+
if (!featureData) {
|
|
2780
|
+
return null;
|
|
2781
|
+
}
|
|
2782
|
+
return this.resolveFeatureResult(featureData);
|
|
2783
|
+
})
|
|
2784
|
+
.filter(Boolean);
|
|
2785
|
+
return {
|
|
2786
|
+
type: 'FeatureCollection',
|
|
2787
|
+
features: featureList,
|
|
2788
|
+
memberCount: memberList.length,
|
|
2789
|
+
};
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2792
|
+
buildRequestParams(featureName, requestConfig) {
|
|
2793
|
+
const typeParam =
|
|
2794
|
+
requestConfig.requestVersion === '2.0.0' ? 'typeNames' : 'typeName';
|
|
2795
|
+
const params = {
|
|
2796
|
+
service: 'WFS',
|
|
2797
|
+
request: 'GetFeature',
|
|
2798
|
+
version: requestConfig.requestVersion,
|
|
2799
|
+
[typeParam]: featureName,
|
|
2800
|
+
srsName: requestConfig.requestSrsName,
|
|
2801
|
+
};
|
|
2802
|
+
if (requestConfig.requestVersion === '2.0.0') {
|
|
2803
|
+
params.count = '1000';
|
|
2804
|
+
} else {
|
|
2805
|
+
params.maxFeatures = '1000';
|
|
2806
|
+
}
|
|
2807
|
+
return params;
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2424
2810
|
async handleNewMapServiceLayer(viewService, serviceType, serviceSelection) {
|
|
2425
2811
|
let resourceLayers = [];
|
|
2426
2812
|
const proxiedUrl = this.buildProxiedUrl(viewService);
|
|
2427
2813
|
try {
|
|
2428
2814
|
const rawUrl = (proxiedUrl || '').trim();
|
|
2429
2815
|
const baseUrl = rawUrl.split('?')[0];
|
|
2816
|
+
const processBboxData = (xml) => {
|
|
2817
|
+
const buildExtentResult = (xmin, ymin, xmax, ymax) => {
|
|
2818
|
+
const numericValues = [xmin, ymin, xmax, ymax].map((value) =>
|
|
2819
|
+
Number(value),
|
|
2820
|
+
);
|
|
2821
|
+
if (numericValues.some((value) => Number.isNaN(value))) {
|
|
2822
|
+
return null;
|
|
2823
|
+
}
|
|
2824
|
+
return {
|
|
2825
|
+
xmin: numericValues[0],
|
|
2826
|
+
ymin: numericValues[1],
|
|
2827
|
+
xmax: numericValues[2],
|
|
2828
|
+
ymax: numericValues[3],
|
|
2829
|
+
spatialReference: { wkid: 4326 },
|
|
2830
|
+
};
|
|
2831
|
+
};
|
|
2832
|
+
const parseCornerValues = (value) => {
|
|
2833
|
+
if (!value || typeof value !== 'string') {
|
|
2834
|
+
return [];
|
|
2835
|
+
}
|
|
2836
|
+
return value
|
|
2837
|
+
.trim()
|
|
2838
|
+
.split(/\s+/)
|
|
2839
|
+
.map((item) => Number(item));
|
|
2840
|
+
};
|
|
2841
|
+
try {
|
|
2842
|
+
let doc = xml;
|
|
2843
|
+
if (typeof xml === 'string') {
|
|
2844
|
+
const parser = new DOMParser();
|
|
2845
|
+
doc = parser.parseFromString(xml, 'text/xml');
|
|
2846
|
+
}
|
|
2847
|
+
if (doc && typeof doc.querySelector === 'function') {
|
|
2848
|
+
const wgs84Node =
|
|
2849
|
+
doc.querySelector('ows\\:WGS84BoundingBox') ||
|
|
2850
|
+
doc.querySelector('WGS84BoundingBox') ||
|
|
2851
|
+
doc.querySelector('wgs84boundingbox');
|
|
2852
|
+
if (wgs84Node) {
|
|
2853
|
+
const lowerCornerNode =
|
|
2854
|
+
wgs84Node.querySelector('ows\\:LowerCorner') ||
|
|
2855
|
+
wgs84Node.querySelector('LowerCorner') ||
|
|
2856
|
+
wgs84Node.querySelector('lowercorner');
|
|
2857
|
+
const upperCornerNode =
|
|
2858
|
+
wgs84Node.querySelector('ows\\:UpperCorner') ||
|
|
2859
|
+
wgs84Node.querySelector('UpperCorner') ||
|
|
2860
|
+
wgs84Node.querySelector('uppercorner');
|
|
2861
|
+
const lowerCorner = parseCornerValues(
|
|
2862
|
+
lowerCornerNode ? lowerCornerNode.textContent : '',
|
|
2863
|
+
);
|
|
2864
|
+
const upperCorner = parseCornerValues(
|
|
2865
|
+
upperCornerNode ? upperCornerNode.textContent : '',
|
|
2866
|
+
);
|
|
2867
|
+
if (lowerCorner.length >= 2 && upperCorner.length >= 2) {
|
|
2868
|
+
const extentResult = buildExtentResult(
|
|
2869
|
+
lowerCorner[0],
|
|
2870
|
+
lowerCorner[1],
|
|
2871
|
+
upperCorner[0],
|
|
2872
|
+
upperCorner[1],
|
|
2873
|
+
);
|
|
2874
|
+
if (extentResult) {
|
|
2875
|
+
return extentResult;
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2880
|
+
const exGeoNode =
|
|
2881
|
+
doc.querySelector('EX_GeographicBoundingBox') ||
|
|
2882
|
+
doc.querySelector('ex_geographicboundingbox');
|
|
2883
|
+
if (exGeoNode) {
|
|
2884
|
+
const westNode =
|
|
2885
|
+
exGeoNode.querySelector('westBoundLongitude') ||
|
|
2886
|
+
exGeoNode.querySelector('ows\\:westBoundLongitude') ||
|
|
2887
|
+
exGeoNode.querySelector('westboundlongitude');
|
|
2888
|
+
const eastNode =
|
|
2889
|
+
exGeoNode.querySelector('eastBoundLongitude') ||
|
|
2890
|
+
exGeoNode.querySelector('ows\\:eastBoundLongitude') ||
|
|
2891
|
+
exGeoNode.querySelector('eastboundlongitude');
|
|
2892
|
+
const southNode =
|
|
2893
|
+
exGeoNode.querySelector('southBoundLatitude') ||
|
|
2894
|
+
exGeoNode.querySelector('ows\\:southBoundLatitude') ||
|
|
2895
|
+
exGeoNode.querySelector('southboundlatitude');
|
|
2896
|
+
const northNode =
|
|
2897
|
+
exGeoNode.querySelector('northBoundLatitude') ||
|
|
2898
|
+
exGeoNode.querySelector('ows\\:northBoundLatitude') ||
|
|
2899
|
+
exGeoNode.querySelector('northboundlatitude');
|
|
2900
|
+
const extentResult = buildExtentResult(
|
|
2901
|
+
westNode ? westNode.textContent : NaN,
|
|
2902
|
+
southNode ? southNode.textContent : NaN,
|
|
2903
|
+
eastNode ? eastNode.textContent : NaN,
|
|
2904
|
+
northNode ? northNode.textContent : NaN,
|
|
2905
|
+
);
|
|
2906
|
+
if (extentResult) {
|
|
2907
|
+
return extentResult;
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
} catch (e) {}
|
|
2912
|
+
|
|
2913
|
+
let xmlText = '';
|
|
2914
|
+
if (typeof xml === 'string') {
|
|
2915
|
+
xmlText = xml;
|
|
2916
|
+
} else if (xml && typeof xml.xml === 'string') {
|
|
2917
|
+
xmlText = xml.xml;
|
|
2918
|
+
}
|
|
2919
|
+
if (xmlText) {
|
|
2920
|
+
const lowerMatch = xmlText.match(
|
|
2921
|
+
/<(?:ows:)?LowerCorner>\s*([^<]+?)\s*<\/(?:ows:)?LowerCorner>/i,
|
|
2922
|
+
);
|
|
2923
|
+
const upperMatch = xmlText.match(
|
|
2924
|
+
/<(?:ows:)?UpperCorner>\s*([^<]+?)\s*<\/(?:ows:)?UpperCorner>/i,
|
|
2925
|
+
);
|
|
2926
|
+
if (lowerMatch && upperMatch) {
|
|
2927
|
+
const lowerCorner = parseCornerValues(lowerMatch[1]);
|
|
2928
|
+
const upperCorner = parseCornerValues(upperMatch[1]);
|
|
2929
|
+
if (lowerCorner.length >= 2 && upperCorner.length >= 2) {
|
|
2930
|
+
const extentResult = buildExtentResult(
|
|
2931
|
+
lowerCorner[0],
|
|
2932
|
+
lowerCorner[1],
|
|
2933
|
+
upperCorner[0],
|
|
2934
|
+
upperCorner[1],
|
|
2935
|
+
);
|
|
2936
|
+
if (extentResult) {
|
|
2937
|
+
return extentResult;
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
|
|
2942
|
+
const westMatch = xmlText.match(
|
|
2943
|
+
/<(?:ows:)?westBoundLongitude>\s*([^<]+?)\s*<\/(?:ows:)?westBoundLongitude>/i,
|
|
2944
|
+
);
|
|
2945
|
+
const eastMatch = xmlText.match(
|
|
2946
|
+
/<(?:ows:)?eastBoundLongitude>\s*([^<]+?)\s*<\/(?:ows:)?eastBoundLongitude>/i,
|
|
2947
|
+
);
|
|
2948
|
+
const southMatch = xmlText.match(
|
|
2949
|
+
/<(?:ows:)?southBoundLatitude>\s*([^<]+?)\s*<\/(?:ows:)?southBoundLatitude>/i,
|
|
2950
|
+
);
|
|
2951
|
+
const northMatch = xmlText.match(
|
|
2952
|
+
/<(?:ows:)?northBoundLatitude>\s*([^<]+?)\s*<\/(?:ows:)?northBoundLatitude>/i,
|
|
2953
|
+
);
|
|
2954
|
+
if (westMatch && eastMatch && southMatch && northMatch) {
|
|
2955
|
+
return buildExtentResult(
|
|
2956
|
+
westMatch[1],
|
|
2957
|
+
southMatch[1],
|
|
2958
|
+
eastMatch[1],
|
|
2959
|
+
northMatch[1],
|
|
2960
|
+
);
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
return null;
|
|
2964
|
+
};
|
|
2430
2965
|
const isWFS =
|
|
2431
2966
|
serviceType === 'WFS' ||
|
|
2432
2967
|
/service=WFS/i.test(rawUrl) ||
|
|
@@ -2449,31 +2984,73 @@ class MenuWidget extends React.Component {
|
|
|
2449
2984
|
}),
|
|
2450
2985
|
];
|
|
2451
2986
|
} else if (isWFS) {
|
|
2452
|
-
|
|
2453
|
-
|
|
2987
|
+
await this.getCapabilities(viewService, 'WFS');
|
|
2988
|
+
const requestConfig = this.resolveRequestConfig(this.xml);
|
|
2989
|
+
const serviceEntries = Object.entries(serviceSelection || {});
|
|
2990
|
+
const layerResults = await Promise.all(
|
|
2991
|
+
serviceEntries.map(async ([name, title]) => {
|
|
2454
2992
|
if (!name) return null;
|
|
2455
|
-
const
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
srsName: 'EPSG:4326',
|
|
2462
|
-
}).toString();
|
|
2463
|
-
const wfsUrl = baseUrl + '?' + params;
|
|
2993
|
+
const requestParams = this.buildRequestParams(name, requestConfig);
|
|
2994
|
+
if (requestConfig.hasJsonFormat && requestConfig.requestFormat) {
|
|
2995
|
+
requestParams.outputFormat = requestConfig.requestFormat;
|
|
2996
|
+
}
|
|
2997
|
+
const requestQuery = new URLSearchParams(requestParams).toString();
|
|
2998
|
+
const requestUrl = baseUrl + '?' + requestQuery;
|
|
2464
2999
|
|
|
2465
3000
|
const id = (name || baseUrl).toUpperCase().replace(/[: ]/g, '_');
|
|
3001
|
+
if (requestConfig.hasJsonFormat) {
|
|
3002
|
+
const layer = new GeoJSONLayer({
|
|
3003
|
+
url: requestUrl,
|
|
3004
|
+
id: id,
|
|
3005
|
+
title: title || name,
|
|
3006
|
+
});
|
|
3007
|
+
layer.LayerId = id;
|
|
3008
|
+
layer.ViewService = baseUrl;
|
|
3009
|
+
layer.name = name;
|
|
3010
|
+
return layer;
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
const xmlResponse = await esriRequest(requestUrl, {
|
|
3014
|
+
responseType: 'text',
|
|
3015
|
+
});
|
|
3016
|
+
if (this.hasExceptionResponse(xmlResponse.data)) {
|
|
3017
|
+
const exceptionMessage = this.resolveExceptionMessage(
|
|
3018
|
+
xmlResponse.data,
|
|
3019
|
+
);
|
|
3020
|
+
throw new Error(exceptionMessage || 'WFS request failed');
|
|
3021
|
+
}
|
|
3022
|
+
const xmlData = this.processXmlData(xmlResponse.data);
|
|
3023
|
+
const featureCollection = this.resolveFeatureCollection(xmlData);
|
|
3024
|
+
if (
|
|
3025
|
+
featureCollection &&
|
|
3026
|
+
featureCollection.memberCount > 0 &&
|
|
3027
|
+
(!featureCollection.features ||
|
|
3028
|
+
featureCollection.features.length === 0)
|
|
3029
|
+
) {
|
|
3030
|
+
throw new Error(
|
|
3031
|
+
'Selected WFS feature type has no geometry and cannot be displayed on map',
|
|
3032
|
+
);
|
|
3033
|
+
}
|
|
3034
|
+
if (!featureCollection || !featureCollection.features?.length) {
|
|
3035
|
+
throw new Error('No WFS features were returned for this layer');
|
|
3036
|
+
}
|
|
3037
|
+
const blobData = new Blob([JSON.stringify(featureCollection)], {
|
|
3038
|
+
type: 'application/json',
|
|
3039
|
+
});
|
|
3040
|
+
const blobUrl = URL.createObjectURL(blobData);
|
|
2466
3041
|
const layer = new GeoJSONLayer({
|
|
2467
|
-
url:
|
|
3042
|
+
url: blobUrl,
|
|
2468
3043
|
id: id,
|
|
2469
3044
|
title: title || name,
|
|
2470
3045
|
});
|
|
2471
3046
|
layer.LayerId = id;
|
|
2472
3047
|
layer.ViewService = baseUrl;
|
|
2473
3048
|
layer.name = name;
|
|
3049
|
+
layer.ServiceDataUrl = blobUrl;
|
|
2474
3050
|
return layer;
|
|
2475
|
-
})
|
|
2476
|
-
|
|
3051
|
+
}),
|
|
3052
|
+
);
|
|
3053
|
+
resourceLayers = layerResults.filter(Boolean);
|
|
2477
3054
|
} else {
|
|
2478
3055
|
await this.getCapabilities(viewService, 'WMS');
|
|
2479
3056
|
const wmsLayers = this.parseWMSLayers(this.xml);
|
|
@@ -2503,8 +3080,16 @@ class MenuWidget extends React.Component {
|
|
|
2503
3080
|
}),
|
|
2504
3081
|
];
|
|
2505
3082
|
}
|
|
3083
|
+
const bboxData = processBboxData(this.xml);
|
|
3084
|
+
if (bboxData && resourceLayers[0]) {
|
|
3085
|
+
resourceLayers[0].fullExtent = bboxData;
|
|
3086
|
+
}
|
|
2506
3087
|
} catch (error) {
|
|
2507
|
-
this.
|
|
3088
|
+
if (this.isNoGeometryError(error)) {
|
|
3089
|
+
this.props.uploadFileErrorHandler('noGeometryError');
|
|
3090
|
+
} else {
|
|
3091
|
+
this.props.uploadFileErrorHandler();
|
|
3092
|
+
}
|
|
2508
3093
|
return;
|
|
2509
3094
|
}
|
|
2510
3095
|
|
|
@@ -2522,7 +3107,9 @@ class MenuWidget extends React.Component {
|
|
|
2522
3107
|
if (typeof resourceLayer.load === 'function' && serviceType === 'WFS') {
|
|
2523
3108
|
try {
|
|
2524
3109
|
await resourceLayer.load();
|
|
2525
|
-
} catch (e) {
|
|
3110
|
+
} catch (e) {
|
|
3111
|
+
throw e;
|
|
3112
|
+
}
|
|
2526
3113
|
}
|
|
2527
3114
|
if (serviceType === 'WMS' || serviceType === 'WMTS') {
|
|
2528
3115
|
const forced = (proxiedUrl || '').trim();
|
|
@@ -2565,7 +3152,11 @@ class MenuWidget extends React.Component {
|
|
|
2565
3152
|
|
|
2566
3153
|
this.props.onServiceChange();
|
|
2567
3154
|
} catch (error) {
|
|
2568
|
-
this.
|
|
3155
|
+
if (this.isNoGeometryError(error)) {
|
|
3156
|
+
this.props.uploadFileErrorHandler('noGeometryError');
|
|
3157
|
+
} else {
|
|
3158
|
+
this.props.uploadFileErrorHandler();
|
|
3159
|
+
}
|
|
2569
3160
|
return;
|
|
2570
3161
|
}
|
|
2571
3162
|
}
|
|
@@ -2612,7 +3203,7 @@ class MenuWidget extends React.Component {
|
|
|
2612
3203
|
this.addUploadedFeatureCollectionToMap(featureCollection, displayTitle);
|
|
2613
3204
|
this.props.onServiceChange && this.props.onServiceChange();
|
|
2614
3205
|
} catch (error) {
|
|
2615
|
-
this.props.uploadFileErrorHandler();
|
|
3206
|
+
this.props.uploadFileErrorHandler(this.resolveUploadErrorType(error));
|
|
2616
3207
|
}
|
|
2617
3208
|
}
|
|
2618
3209
|
|
|
@@ -2699,12 +3290,18 @@ class MenuWidget extends React.Component {
|
|
|
2699
3290
|
createUserServices(serviceLayers) {
|
|
2700
3291
|
const fieldset = document.getElementById('map-menu-services');
|
|
2701
3292
|
if (!fieldset) return;
|
|
3293
|
+
const processTitleData = (titleText) => {
|
|
3294
|
+
if (!titleText) return titleText;
|
|
3295
|
+
if (titleText.length <= 33) return titleText;
|
|
3296
|
+
return titleText.slice(0, 33) + '...';
|
|
3297
|
+
};
|
|
2702
3298
|
|
|
2703
3299
|
// Create an array of all layer elements
|
|
2704
3300
|
const layerElements = serviceLayers.map((layer, index) => {
|
|
2705
3301
|
const { LayerId, title, description } = layer;
|
|
2706
3302
|
const parentIndex = this.layers[layer.id];
|
|
2707
3303
|
const checkboxId = LayerId;
|
|
3304
|
+
const displayTitle = processTitleData(title || `Layer ${index + 1}`);
|
|
2708
3305
|
|
|
2709
3306
|
return (
|
|
2710
3307
|
<div
|
|
@@ -2740,14 +3337,14 @@ class MenuWidget extends React.Component {
|
|
|
2740
3337
|
<legend className="ccl-form-legend">
|
|
2741
3338
|
{description ? (
|
|
2742
3339
|
<Popup
|
|
2743
|
-
trigger={<span>{
|
|
3340
|
+
trigger={<span>{displayTitle}</span>}
|
|
2744
3341
|
content={description}
|
|
2745
3342
|
basic
|
|
2746
3343
|
className="custom"
|
|
2747
3344
|
style={{ transform: 'translateX(-4rem)' }}
|
|
2748
3345
|
/>
|
|
2749
3346
|
) : (
|
|
2750
|
-
<span>{
|
|
3347
|
+
<span>{displayTitle}</span>
|
|
2751
3348
|
)}
|
|
2752
3349
|
</legend>
|
|
2753
3350
|
</label>
|
|
@@ -2782,7 +3379,14 @@ class MenuWidget extends React.Component {
|
|
|
2782
3379
|
}
|
|
2783
3380
|
|
|
2784
3381
|
// Delete from layers object
|
|
2785
|
-
if (this.layers[elemId])
|
|
3382
|
+
if (this.layers[elemId]) {
|
|
3383
|
+
if (this.layers[elemId].ServiceDataUrl) {
|
|
3384
|
+
try {
|
|
3385
|
+
URL.revokeObjectURL(this.layers[elemId].ServiceDataUrl);
|
|
3386
|
+
} catch (e) {}
|
|
3387
|
+
}
|
|
3388
|
+
delete this.layers[elemId];
|
|
3389
|
+
}
|
|
2786
3390
|
|
|
2787
3391
|
// Remove from ArcGIS map
|
|
2788
3392
|
let removeLayer = this.props.map.findLayerById(elemId) || null;
|
|
@@ -4305,7 +4909,7 @@ class MenuWidget extends React.Component {
|
|
|
4305
4909
|
// Get the coordinates of the click on the view
|
|
4306
4910
|
const proxiedUrl = this.buildProxiedUrl(url);
|
|
4307
4911
|
return esriRequest(proxiedUrl, {
|
|
4308
|
-
responseType: '
|
|
4912
|
+
responseType: 'text',
|
|
4309
4913
|
sync: 'true',
|
|
4310
4914
|
query: {
|
|
4311
4915
|
request: 'GetCapabilities',
|
|
@@ -4315,7 +4919,7 @@ class MenuWidget extends React.Component {
|
|
|
4315
4919
|
.then((response) => {
|
|
4316
4920
|
const xmlDoc = response.data;
|
|
4317
4921
|
const parser = new DOMParser();
|
|
4318
|
-
this.xml = parser.parseFromString(xmlDoc, 'text/
|
|
4922
|
+
this.xml = parser.parseFromString(xmlDoc, 'text/xml');
|
|
4319
4923
|
})
|
|
4320
4924
|
.catch(() => {});
|
|
4321
4925
|
};
|
|
@@ -4654,9 +5258,36 @@ class MenuWidget extends React.Component {
|
|
|
4654
5258
|
});
|
|
4655
5259
|
this.view.goTo(myExtent);
|
|
4656
5260
|
}
|
|
5261
|
+
} else if (serviceLayer) {
|
|
5262
|
+
const storedServiceExtent =
|
|
5263
|
+
this.layers[elem.id]?.fullExtent ||
|
|
5264
|
+
serviceLayer.fullExtent ||
|
|
5265
|
+
(this.layers[elem.id]?.fullExtents &&
|
|
5266
|
+
this.layers[elem.id]?.fullExtents[0]
|
|
5267
|
+
? this.layers[elem.id]?.fullExtents[0]
|
|
5268
|
+
: null) ||
|
|
5269
|
+
(serviceLayer.fullExtents && serviceLayer.fullExtents[0]
|
|
5270
|
+
? serviceLayer.fullExtents[0]
|
|
5271
|
+
: null);
|
|
5272
|
+
if (
|
|
5273
|
+
storedServiceExtent &&
|
|
5274
|
+
storedServiceExtent.xmin !== undefined &&
|
|
5275
|
+
storedServiceExtent.ymin !== undefined &&
|
|
5276
|
+
storedServiceExtent.xmax !== undefined &&
|
|
5277
|
+
storedServiceExtent.ymax !== undefined
|
|
5278
|
+
) {
|
|
5279
|
+
BBoxes = {
|
|
5280
|
+
dataset: {
|
|
5281
|
+
xmin: Number(storedServiceExtent.xmin),
|
|
5282
|
+
ymin: Number(storedServiceExtent.ymin),
|
|
5283
|
+
xmax: Number(storedServiceExtent.xmax),
|
|
5284
|
+
ymax: Number(storedServiceExtent.ymax),
|
|
5285
|
+
},
|
|
5286
|
+
};
|
|
5287
|
+
}
|
|
4657
5288
|
} else if (this.url?.toLowerCase().endsWith('mapserver')) {
|
|
4658
5289
|
BBoxes = await this.parseBBOXMAPSERVER(this.layers[elem.id]);
|
|
4659
|
-
} else if (this.url?.toLowerCase().includes('wms')
|
|
5290
|
+
} else if (this.url?.toLowerCase().includes('wms')) {
|
|
4660
5291
|
await this.getCapabilities(this.url, 'wms');
|
|
4661
5292
|
BBoxes = this.parseBBOXWMS(this.xml);
|
|
4662
5293
|
} else if (this.url?.toLowerCase().includes('wmts')) {
|
|
@@ -150,8 +150,32 @@ class UploadWidget extends React.Component {
|
|
|
150
150
|
this.setState({ serviceUrl: event.target.value });
|
|
151
151
|
};
|
|
152
152
|
|
|
153
|
+
buildUploadResetState = () => {
|
|
154
|
+
return {
|
|
155
|
+
serviceUrl: '',
|
|
156
|
+
wfsFeatures: {},
|
|
157
|
+
selectedFeatures: {},
|
|
158
|
+
selectedFile: null,
|
|
159
|
+
showInfoPopup: false,
|
|
160
|
+
infoPopupType: '',
|
|
161
|
+
globalDragActive: false,
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
|
|
153
165
|
handleServiceTypeChange = (event) => {
|
|
154
|
-
|
|
166
|
+
const nextServiceType = event.target.value;
|
|
167
|
+
this.setState((prevState) => {
|
|
168
|
+
if (prevState.selectedServiceType === nextServiceType) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
selectedServiceType: nextServiceType,
|
|
173
|
+
...this.buildUploadResetState(),
|
|
174
|
+
};
|
|
175
|
+
});
|
|
176
|
+
if (this.fileInput && this.fileInput.current) {
|
|
177
|
+
this.fileInput.current.value = null;
|
|
178
|
+
}
|
|
155
179
|
};
|
|
156
180
|
|
|
157
181
|
setActiveTab(tab) {
|
|
@@ -222,6 +246,42 @@ class UploadWidget extends React.Component {
|
|
|
222
246
|
}
|
|
223
247
|
};
|
|
224
248
|
|
|
249
|
+
getServiceTypeFromUrl = (serviceUrl) => {
|
|
250
|
+
try {
|
|
251
|
+
const parsedUrl = new URL(serviceUrl);
|
|
252
|
+
const queryService = (parsedUrl.searchParams.get('service') || '')
|
|
253
|
+
.trim()
|
|
254
|
+
.toUpperCase();
|
|
255
|
+
if (
|
|
256
|
+
queryService === 'WMS' ||
|
|
257
|
+
queryService === 'WMTS' ||
|
|
258
|
+
queryService === 'WFS'
|
|
259
|
+
) {
|
|
260
|
+
return queryService;
|
|
261
|
+
}
|
|
262
|
+
const encodedUrl = (
|
|
263
|
+
(parsedUrl.hostname || '') + (parsedUrl.pathname || '')
|
|
264
|
+
).toLowerCase();
|
|
265
|
+
const serviceMatch = encodedUrl.match(
|
|
266
|
+
/(^|[^a-z])(wmts|wms|wfs)([^a-z]|$)/i,
|
|
267
|
+
);
|
|
268
|
+
if (serviceMatch && serviceMatch[2]) {
|
|
269
|
+
return serviceMatch[2].toUpperCase();
|
|
270
|
+
}
|
|
271
|
+
return null;
|
|
272
|
+
} catch (e) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
isServiceTypeMatchingUrl = (serviceUrl, selectedServiceType) => {
|
|
278
|
+
const encodedServiceType = this.getServiceTypeFromUrl(serviceUrl);
|
|
279
|
+
if (!encodedServiceType) {
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
return encodedServiceType === selectedServiceType;
|
|
283
|
+
};
|
|
284
|
+
|
|
225
285
|
stripProtocol = (url) => {
|
|
226
286
|
return (url || '').replace(/^https?:\/\//i, '');
|
|
227
287
|
};
|
|
@@ -236,11 +296,163 @@ class UploadWidget extends React.Component {
|
|
|
236
296
|
return this.getProxyBase() + this.stripProtocol(url);
|
|
237
297
|
};
|
|
238
298
|
|
|
299
|
+
resolveNodeValue = (nodeValue) => {
|
|
300
|
+
if (nodeValue === null || nodeValue === undefined) {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
if (typeof nodeValue === 'string' || typeof nodeValue === 'number') {
|
|
304
|
+
const textValue = String(nodeValue).trim();
|
|
305
|
+
return textValue === '' ? null : textValue;
|
|
306
|
+
}
|
|
307
|
+
if (Array.isArray(nodeValue)) {
|
|
308
|
+
for (let i = 0; i < nodeValue.length; i += 1) {
|
|
309
|
+
const resolvedValue = this.resolveNodeValue(nodeValue[i]);
|
|
310
|
+
if (resolvedValue) {
|
|
311
|
+
return resolvedValue;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
if (typeof nodeValue === 'object') {
|
|
317
|
+
if (Object.prototype.hasOwnProperty.call(nodeValue, '#text')) {
|
|
318
|
+
return this.resolveNodeValue(nodeValue['#text']);
|
|
319
|
+
}
|
|
320
|
+
if (Object.prototype.hasOwnProperty.call(nodeValue, '_')) {
|
|
321
|
+
return this.resolveNodeValue(nodeValue._);
|
|
322
|
+
}
|
|
323
|
+
if (Object.prototype.hasOwnProperty.call(nodeValue, '$t')) {
|
|
324
|
+
return this.resolveNodeValue(nodeValue.$t);
|
|
325
|
+
}
|
|
326
|
+
if (Object.prototype.hasOwnProperty.call(nodeValue, 'value')) {
|
|
327
|
+
return this.resolveNodeValue(nodeValue.value);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return null;
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
processXmlCapabilitiesData = (xmlInput) => {
|
|
334
|
+
let xmlDoc = xmlInput;
|
|
335
|
+
if (typeof xmlInput === 'string') {
|
|
336
|
+
const parser = new DOMParser();
|
|
337
|
+
xmlDoc = parser.parseFromString(xmlInput, 'text/xml');
|
|
338
|
+
}
|
|
339
|
+
if (!xmlDoc || typeof xmlDoc.querySelectorAll !== 'function') {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
const featureTypeList = [];
|
|
343
|
+
const featureTypes = xmlDoc.querySelectorAll('FeatureType, featuretype');
|
|
344
|
+
featureTypes.forEach((featureTypeNode) => {
|
|
345
|
+
const titleNode =
|
|
346
|
+
featureTypeNode.querySelector('Title') ||
|
|
347
|
+
featureTypeNode.querySelector('title') ||
|
|
348
|
+
featureTypeNode.querySelector('ows\\:Title');
|
|
349
|
+
const nameNode =
|
|
350
|
+
featureTypeNode.querySelector('Name') ||
|
|
351
|
+
featureTypeNode.querySelector('name') ||
|
|
352
|
+
featureTypeNode.querySelector('wfs\\:Name') ||
|
|
353
|
+
featureTypeNode.querySelector('ows\\:Identifier');
|
|
354
|
+
const nameValue = this.resolveNodeValue(nameNode && nameNode.textContent);
|
|
355
|
+
const titleValue = this.resolveNodeValue(
|
|
356
|
+
titleNode && titleNode.textContent,
|
|
357
|
+
);
|
|
358
|
+
if (nameValue) {
|
|
359
|
+
featureTypeList.push({
|
|
360
|
+
Name: nameValue,
|
|
361
|
+
Title: titleValue,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
return {
|
|
366
|
+
FeatureTypeList: {
|
|
367
|
+
FeatureType: featureTypeList,
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
resolveCapabilitiesData = (capabilitiesResponse) => {
|
|
373
|
+
if (
|
|
374
|
+
capabilitiesResponse &&
|
|
375
|
+
typeof capabilitiesResponse === 'object' &&
|
|
376
|
+
!Array.isArray(capabilitiesResponse) &&
|
|
377
|
+
typeof capabilitiesResponse.querySelectorAll !== 'function'
|
|
378
|
+
) {
|
|
379
|
+
return capabilitiesResponse;
|
|
380
|
+
}
|
|
381
|
+
if (typeof capabilitiesResponse === 'string') {
|
|
382
|
+
const trimmedResponse = capabilitiesResponse.trim();
|
|
383
|
+
if (trimmedResponse === '') {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
try {
|
|
387
|
+
return JSON.parse(trimmedResponse);
|
|
388
|
+
} catch (e) {
|
|
389
|
+
return this.processXmlCapabilitiesData(trimmedResponse);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (
|
|
393
|
+
capabilitiesResponse &&
|
|
394
|
+
typeof capabilitiesResponse === 'object' &&
|
|
395
|
+
typeof capabilitiesResponse.querySelectorAll === 'function'
|
|
396
|
+
) {
|
|
397
|
+
return this.processXmlCapabilitiesData(capabilitiesResponse);
|
|
398
|
+
}
|
|
399
|
+
return null;
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
resolveFeatureTypeList = (capabilitiesData) => {
|
|
403
|
+
const featureTypeList = [];
|
|
404
|
+
const processNode = (nodeValue) => {
|
|
405
|
+
if (nodeValue === null || nodeValue === undefined) {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (Array.isArray(nodeValue)) {
|
|
409
|
+
nodeValue.forEach((itemValue) => {
|
|
410
|
+
processNode(itemValue);
|
|
411
|
+
});
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
if (typeof nodeValue !== 'object') {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const nameValue = this.resolveNodeValue(
|
|
418
|
+
nodeValue.Name ||
|
|
419
|
+
nodeValue.name ||
|
|
420
|
+
nodeValue['wfs:Name'] ||
|
|
421
|
+
nodeValue['ows:Identifier'],
|
|
422
|
+
);
|
|
423
|
+
const titleValue = this.resolveNodeValue(
|
|
424
|
+
nodeValue.Title || nodeValue.title || nodeValue['ows:Title'],
|
|
425
|
+
);
|
|
426
|
+
if (nameValue) {
|
|
427
|
+
featureTypeList.push({
|
|
428
|
+
Name: nameValue,
|
|
429
|
+
Title: titleValue,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
const nestedFeatureType =
|
|
433
|
+
nodeValue.FeatureType || nodeValue.featureType || nodeValue.featuretype;
|
|
434
|
+
if (nestedFeatureType) {
|
|
435
|
+
processNode(nestedFeatureType);
|
|
436
|
+
}
|
|
437
|
+
Object.keys(nodeValue).forEach((keyValue) => {
|
|
438
|
+
if (
|
|
439
|
+
keyValue !== 'FeatureType' &&
|
|
440
|
+
keyValue !== 'featureType' &&
|
|
441
|
+
keyValue !== 'featuretype'
|
|
442
|
+
) {
|
|
443
|
+
processNode(nodeValue[keyValue]);
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
};
|
|
447
|
+
processNode(capabilitiesData);
|
|
448
|
+
return featureTypeList;
|
|
449
|
+
};
|
|
450
|
+
|
|
239
451
|
getCapabilities = (url, serviceType) => {
|
|
240
452
|
// Get the coordinates of the click on the view
|
|
241
453
|
const proxiedUrl = this.buildProxiedUrl(url);
|
|
242
454
|
return esriRequest(proxiedUrl, {
|
|
243
|
-
responseType: '
|
|
455
|
+
responseType: 'text',
|
|
244
456
|
query: {
|
|
245
457
|
request: 'GetCapabilities',
|
|
246
458
|
service: serviceType,
|
|
@@ -248,34 +460,19 @@ class UploadWidget extends React.Component {
|
|
|
248
460
|
},
|
|
249
461
|
})
|
|
250
462
|
.then((response) => {
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
this.xml = parser.parseFromString(xmlDoc, 'text/html');
|
|
463
|
+
const capabilitiesData = this.resolveCapabilitiesData(response.data);
|
|
464
|
+
this.capabilitiesData = capabilitiesData;
|
|
254
465
|
})
|
|
255
466
|
.catch(() => {});
|
|
256
467
|
};
|
|
257
468
|
|
|
258
|
-
parseWFSFeatures = (
|
|
259
|
-
let doc = xml;
|
|
469
|
+
parseWFSFeatures = (capabilitiesData) => {
|
|
260
470
|
try {
|
|
261
|
-
if (typeof xml === 'string') {
|
|
262
|
-
const parser = new DOMParser();
|
|
263
|
-
doc = parser.parseFromString(xml, 'text/xml');
|
|
264
|
-
}
|
|
265
471
|
const features = {};
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
ft.querySelector('title') ||
|
|
271
|
-
ft.querySelector('ows\\:Title');
|
|
272
|
-
const nameEl =
|
|
273
|
-
ft.querySelector('Name') ||
|
|
274
|
-
ft.querySelector('name') ||
|
|
275
|
-
ft.querySelector('wfs\\:Name') ||
|
|
276
|
-
ft.querySelector('ows\\:Identifier');
|
|
277
|
-
const key = nameEl ? (nameEl.textContent || '').trim() : null;
|
|
278
|
-
const title = titleEl ? (titleEl.textContent || '').trim() : null;
|
|
472
|
+
const featureTypeList = this.resolveFeatureTypeList(capabilitiesData);
|
|
473
|
+
featureTypeList.forEach((featureTypeValue) => {
|
|
474
|
+
const key = this.resolveNodeValue(featureTypeValue.Name);
|
|
475
|
+
const title = this.resolveNodeValue(featureTypeValue.Title);
|
|
279
476
|
if (key) {
|
|
280
477
|
features[key] = title ?? null;
|
|
281
478
|
}
|
|
@@ -310,14 +507,15 @@ class UploadWidget extends React.Component {
|
|
|
310
507
|
selectedServiceType === 'WFS' &&
|
|
311
508
|
serviceUrl &&
|
|
312
509
|
serviceUrl.trim() !== '' &&
|
|
313
|
-
this.isValidUrl(serviceUrl)
|
|
510
|
+
this.isValidUrl(serviceUrl) &&
|
|
511
|
+
this.isServiceTypeMatchingUrl(serviceUrl, selectedServiceType)
|
|
314
512
|
) {
|
|
315
513
|
const normalizedUrl = this.getNormalizedUrlForType(
|
|
316
514
|
serviceUrl,
|
|
317
515
|
selectedServiceType,
|
|
318
516
|
);
|
|
319
517
|
await this.getCapabilities(normalizedUrl, selectedServiceType);
|
|
320
|
-
const result = this.parseWFSFeatures(this.
|
|
518
|
+
const result = this.parseWFSFeatures(this.capabilitiesData);
|
|
321
519
|
this.setState(() => ({
|
|
322
520
|
wfsFeatures: result,
|
|
323
521
|
}));
|
|
@@ -334,7 +532,8 @@ class UploadWidget extends React.Component {
|
|
|
334
532
|
selectedServiceType &&
|
|
335
533
|
serviceUrl &&
|
|
336
534
|
serviceUrl.trim() !== '' &&
|
|
337
|
-
this.isValidUrl(serviceUrl)
|
|
535
|
+
this.isValidUrl(serviceUrl) &&
|
|
536
|
+
this.isServiceTypeMatchingUrl(serviceUrl, selectedServiceType)
|
|
338
537
|
) {
|
|
339
538
|
const normalizedUrl = this.getNormalizedUrlForType(
|
|
340
539
|
serviceUrl,
|
|
@@ -375,10 +574,10 @@ class UploadWidget extends React.Component {
|
|
|
375
574
|
this.setState({ wfsFeatures: {}, serviceUrl: '', selectedFeatures: {} });
|
|
376
575
|
};
|
|
377
576
|
|
|
378
|
-
errorPopup = () => {
|
|
577
|
+
errorPopup = (popupType = 'uploadError') => {
|
|
379
578
|
this.setState({
|
|
380
579
|
showInfoPopup: true,
|
|
381
|
-
infoPopupType:
|
|
580
|
+
infoPopupType: popupType,
|
|
382
581
|
});
|
|
383
582
|
setTimeout(() => {
|
|
384
583
|
this.setState({
|
|
@@ -456,7 +655,7 @@ class UploadWidget extends React.Component {
|
|
|
456
655
|
|
|
457
656
|
componentDidUpdate(prevProps, prevState) {
|
|
458
657
|
if (!prevProps.showErrorPopup && this.props.showErrorPopup) {
|
|
459
|
-
this.errorPopup();
|
|
658
|
+
this.errorPopup(this.props.showErrorPopupType || 'uploadError');
|
|
460
659
|
}
|
|
461
660
|
}
|
|
462
661
|
|
|
@@ -723,6 +922,38 @@ class UploadWidget extends React.Component {
|
|
|
723
922
|
</div>
|
|
724
923
|
</>
|
|
725
924
|
)}
|
|
925
|
+
{this.state.infoPopupType === 'fileLimit' && (
|
|
926
|
+
<>
|
|
927
|
+
<span className="drawRectanglePopup-icon">
|
|
928
|
+
<FontAwesomeIcon icon={['fas', 'info-circle']} />
|
|
929
|
+
</span>
|
|
930
|
+
<div className="drawRectanglePopup-text">
|
|
931
|
+
File exceeds the max size allowed of 10MB.
|
|
932
|
+
</div>
|
|
933
|
+
</>
|
|
934
|
+
)}
|
|
935
|
+
{this.state.infoPopupType === 'shapefileLimit' && (
|
|
936
|
+
<>
|
|
937
|
+
<span className="drawRectanglePopup-icon">
|
|
938
|
+
<FontAwesomeIcon icon={['fas', 'info-circle']} />
|
|
939
|
+
</span>
|
|
940
|
+
<div className="drawRectanglePopup-text">
|
|
941
|
+
Uploading shapefiles files larger than 2MB is not
|
|
942
|
+
allowed.
|
|
943
|
+
</div>
|
|
944
|
+
</>
|
|
945
|
+
)}
|
|
946
|
+
{this.state.infoPopupType === 'noGeometryError' && (
|
|
947
|
+
<>
|
|
948
|
+
<span className="drawRectanglePopup-icon">
|
|
949
|
+
<FontAwesomeIcon icon={['fas', 'info-circle']} />
|
|
950
|
+
</span>
|
|
951
|
+
<div className="drawRectanglePopup-text">
|
|
952
|
+
Selected service has no geometry data and cannot be
|
|
953
|
+
displayed on the map.
|
|
954
|
+
</div>
|
|
955
|
+
</>
|
|
956
|
+
)}
|
|
726
957
|
</div>
|
|
727
958
|
</div>
|
|
728
959
|
</div>
|
|
@@ -833,9 +1064,6 @@ class UploadWidget extends React.Component {
|
|
|
833
1064
|
disabled={!selectedFile}
|
|
834
1065
|
style={{
|
|
835
1066
|
width: '100%',
|
|
836
|
-
border: '2px solid #c0d36b',
|
|
837
|
-
background: '#ffffff',
|
|
838
|
-
color: '#8ea92a',
|
|
839
1067
|
}}
|
|
840
1068
|
onClick={this.handleAddClick}
|
|
841
1069
|
>
|
|
@@ -847,9 +1075,6 @@ class UploadWidget extends React.Component {
|
|
|
847
1075
|
disabled={false}
|
|
848
1076
|
style={{
|
|
849
1077
|
width: '100%',
|
|
850
|
-
border: '2px solid #c0d36b',
|
|
851
|
-
background: '#ffffff',
|
|
852
|
-
color: '#8ea92a',
|
|
853
1078
|
}}
|
|
854
1079
|
onClick={this.handleBrowseClick}
|
|
855
1080
|
>
|
|
@@ -685,6 +685,19 @@ div.upload-container
|
|
|
685
685
|
cursor: default;
|
|
686
686
|
}
|
|
687
687
|
|
|
688
|
+
.upload-container .esri-button {
|
|
689
|
+
border: 1px solid #a0b128;
|
|
690
|
+
background-color: #a0b128;
|
|
691
|
+
color: white;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
.upload-container .esri-button:enabled:hover,
|
|
695
|
+
.upload-container .esri-button[enabled]:hover {
|
|
696
|
+
border: 1px solid #a0b128;
|
|
697
|
+
background-color: white;
|
|
698
|
+
color: #a0b128;
|
|
699
|
+
}
|
|
700
|
+
|
|
688
701
|
.upload-panel {
|
|
689
702
|
display: unset !important;
|
|
690
703
|
}
|
|
@@ -999,6 +1012,35 @@ div.upload-container
|
|
|
999
1012
|
margin-left: 1.75rem;
|
|
1000
1013
|
}
|
|
1001
1014
|
|
|
1015
|
+
.land #map-menu-services .map-menu-service {
|
|
1016
|
+
width: 100%;
|
|
1017
|
+
align-items: center;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
.land #map-menu-services .ccl-expandable__button .dropdown-icon {
|
|
1021
|
+
display: flex;
|
|
1022
|
+
width: 100%;
|
|
1023
|
+
height: auto;
|
|
1024
|
+
margin-right: 0;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
.land #map-menu-services .map-menu-service .ccl-form-check-label {
|
|
1028
|
+
min-width: 0;
|
|
1029
|
+
flex: 1;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
.land #map-menu-services .map-menu-service .ccl-form-legend {
|
|
1033
|
+
display: block;
|
|
1034
|
+
overflow: hidden;
|
|
1035
|
+
text-overflow: ellipsis;
|
|
1036
|
+
white-space: nowrap;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
#map-menu-services .map-menu-icon.map-menu-service-icon {
|
|
1040
|
+
margin-right: 0;
|
|
1041
|
+
margin-left: auto;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1002
1044
|
.map-menu-dropdown i:hover {
|
|
1003
1045
|
color: black;
|
|
1004
1046
|
}
|
|
@@ -1941,16 +1983,13 @@ div.map-container.popup-block {
|
|
|
1941
1983
|
}
|
|
1942
1984
|
|
|
1943
1985
|
.esri-popup__icon {
|
|
1944
|
-
color:
|
|
1986
|
+
color: auto;
|
|
1945
1987
|
}
|
|
1946
1988
|
|
|
1947
1989
|
.esri-widget__anchor {
|
|
1948
1990
|
color: #7c8921 !important;
|
|
1949
1991
|
outline: 0px !important;
|
|
1950
1992
|
}
|
|
1951
|
-
/* .esri-widget__anchor:focus {
|
|
1952
|
-
background-color: #a0b128 !important;
|
|
1953
|
-
} */
|
|
1954
1993
|
|
|
1955
1994
|
.esri-popup__header-container {
|
|
1956
1995
|
background-color: #a0b128 !important;
|
|
@@ -1963,5 +2002,16 @@ div.map-container.popup-block {
|
|
|
1963
2002
|
|
|
1964
2003
|
.esri-popup__button {
|
|
1965
2004
|
background-color: #a0b128 !important;
|
|
2005
|
+
color: white !important;
|
|
1966
2006
|
outline: 0px !important;
|
|
1967
2007
|
}
|
|
2008
|
+
|
|
2009
|
+
.esri-popup__button:hover {
|
|
2010
|
+
background-color: #7c8921 !important;
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
.esri-popup__action:hover {
|
|
2014
|
+
background-color: white !important;
|
|
2015
|
+
color: #a0b128 !important;
|
|
2016
|
+
outline: solid 2px #a0b128 !important;
|
|
2017
|
+
}
|