umap-project 2.4.1__py3-none-any.whl → 2.5.0__py3-none-any.whl
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.
- umap/__init__.py +1 -1
- umap/locale/el/LC_MESSAGES/django.mo +0 -0
- umap/locale/el/LC_MESSAGES/django.po +145 -90
- umap/locale/en/LC_MESSAGES/django.po +13 -13
- umap/locale/eu/LC_MESSAGES/django.mo +0 -0
- umap/locale/eu/LC_MESSAGES/django.po +145 -89
- umap/locale/hu/LC_MESSAGES/django.mo +0 -0
- umap/locale/hu/LC_MESSAGES/django.po +100 -50
- umap/static/umap/base.css +5 -2
- umap/static/umap/content.css +2 -2
- umap/static/umap/css/contextmenu.css +11 -0
- umap/static/umap/css/dialog.css +25 -4
- umap/static/umap/css/importers.css +2 -0
- umap/static/umap/css/panel.css +6 -4
- umap/static/umap/css/slideshow.css +69 -0
- umap/static/umap/css/tableeditor.css +69 -0
- umap/static/umap/css/tooltip.css +3 -3
- umap/static/umap/img/16-white.svg +4 -0
- umap/static/umap/img/source/16-white.svg +5 -1
- umap/static/umap/js/components/alerts/alert.css +11 -11
- umap/static/umap/js/components/alerts/alert.js +1 -1
- umap/static/umap/js/modules/autocomplete.js +27 -5
- umap/static/umap/js/modules/browser.js +20 -14
- umap/static/umap/js/modules/caption.js +4 -4
- umap/static/umap/js/modules/dompurify.js +2 -3
- umap/static/umap/js/modules/facets.js +53 -17
- umap/static/umap/js/modules/formatter.js +153 -0
- umap/static/umap/js/modules/global.js +25 -16
- umap/static/umap/js/modules/help.js +26 -26
- umap/static/umap/js/modules/importer.js +10 -10
- umap/static/umap/js/modules/importers/communesfr.js +3 -1
- umap/static/umap/js/modules/importers/datasets.js +8 -6
- umap/static/umap/js/modules/importers/geodatamine.js +14 -14
- umap/static/umap/js/modules/importers/overpass.js +19 -15
- umap/static/umap/js/modules/orderable.js +2 -2
- umap/static/umap/js/modules/request.js +1 -1
- umap/static/umap/js/modules/rules.js +26 -11
- umap/static/umap/js/modules/schema.js +16 -12
- umap/static/umap/js/{umap.share.js → modules/share.js} +58 -103
- umap/static/umap/js/modules/slideshow.js +141 -0
- umap/static/umap/js/modules/sync/engine.js +3 -3
- umap/static/umap/js/modules/sync/updaters.js +10 -11
- umap/static/umap/js/modules/sync/websocket.js +1 -1
- umap/static/umap/js/modules/tableeditor.js +329 -0
- umap/static/umap/js/modules/ui/base.js +93 -0
- umap/static/umap/js/modules/ui/contextmenu.js +50 -0
- umap/static/umap/js/modules/ui/dialog.js +169 -31
- umap/static/umap/js/modules/ui/panel.js +7 -5
- umap/static/umap/js/modules/ui/tooltip.js +7 -77
- umap/static/umap/js/modules/urls.js +1 -2
- umap/static/umap/js/modules/utils.js +36 -16
- umap/static/umap/js/umap.controls.js +27 -29
- umap/static/umap/js/umap.core.js +19 -15
- umap/static/umap/js/umap.datalayer.permissions.js +15 -18
- umap/static/umap/js/umap.features.js +113 -131
- umap/static/umap/js/umap.forms.js +203 -228
- umap/static/umap/js/umap.icon.js +17 -22
- umap/static/umap/js/umap.js +117 -107
- umap/static/umap/js/umap.layer.js +374 -324
- umap/static/umap/js/umap.permissions.js +7 -10
- umap/static/umap/js/umap.popup.js +20 -20
- umap/static/umap/locale/am_ET.js +22 -5
- umap/static/umap/locale/am_ET.json +22 -5
- umap/static/umap/locale/ar.js +22 -5
- umap/static/umap/locale/ar.json +22 -5
- umap/static/umap/locale/ast.js +22 -5
- umap/static/umap/locale/ast.json +22 -5
- umap/static/umap/locale/bg.js +22 -5
- umap/static/umap/locale/bg.json +22 -5
- umap/static/umap/locale/br.js +22 -5
- umap/static/umap/locale/br.json +22 -5
- umap/static/umap/locale/ca.js +56 -39
- umap/static/umap/locale/ca.json +56 -39
- umap/static/umap/locale/cs_CZ.js +22 -5
- umap/static/umap/locale/cs_CZ.json +22 -5
- umap/static/umap/locale/da.js +22 -5
- umap/static/umap/locale/da.json +22 -5
- umap/static/umap/locale/de.js +22 -5
- umap/static/umap/locale/de.json +22 -5
- umap/static/umap/locale/el.js +27 -10
- umap/static/umap/locale/el.json +27 -10
- umap/static/umap/locale/en.js +22 -6
- umap/static/umap/locale/en.json +22 -6
- umap/static/umap/locale/en_US.json +22 -5
- umap/static/umap/locale/es.js +22 -6
- umap/static/umap/locale/es.json +22 -6
- umap/static/umap/locale/et.js +22 -5
- umap/static/umap/locale/et.json +22 -5
- umap/static/umap/locale/eu.js +167 -150
- umap/static/umap/locale/eu.json +167 -150
- umap/static/umap/locale/fa_IR.js +22 -5
- umap/static/umap/locale/fa_IR.json +22 -5
- umap/static/umap/locale/fi.js +22 -5
- umap/static/umap/locale/fi.json +22 -5
- umap/static/umap/locale/fr.js +22 -6
- umap/static/umap/locale/fr.json +22 -6
- umap/static/umap/locale/gl.js +22 -5
- umap/static/umap/locale/gl.json +22 -5
- umap/static/umap/locale/he.js +22 -5
- umap/static/umap/locale/he.json +22 -5
- umap/static/umap/locale/hr.js +22 -5
- umap/static/umap/locale/hr.json +22 -5
- umap/static/umap/locale/hu.js +89 -72
- umap/static/umap/locale/hu.json +89 -72
- umap/static/umap/locale/id.js +22 -5
- umap/static/umap/locale/id.json +22 -5
- umap/static/umap/locale/is.js +22 -5
- umap/static/umap/locale/is.json +22 -5
- umap/static/umap/locale/it.js +22 -5
- umap/static/umap/locale/it.json +22 -5
- umap/static/umap/locale/ja.js +22 -5
- umap/static/umap/locale/ja.json +22 -5
- umap/static/umap/locale/ko.js +22 -5
- umap/static/umap/locale/ko.json +22 -5
- umap/static/umap/locale/lt.js +22 -5
- umap/static/umap/locale/lt.json +22 -5
- umap/static/umap/locale/ms.js +22 -5
- umap/static/umap/locale/ms.json +22 -5
- umap/static/umap/locale/nl.js +22 -5
- umap/static/umap/locale/nl.json +22 -5
- umap/static/umap/locale/no.js +22 -5
- umap/static/umap/locale/no.json +22 -5
- umap/static/umap/locale/pl.js +22 -5
- umap/static/umap/locale/pl.json +22 -5
- umap/static/umap/locale/pl_PL.json +22 -5
- umap/static/umap/locale/pt.js +22 -6
- umap/static/umap/locale/pt.json +22 -6
- umap/static/umap/locale/pt_BR.js +22 -5
- umap/static/umap/locale/pt_BR.json +22 -5
- umap/static/umap/locale/pt_PT.js +22 -5
- umap/static/umap/locale/pt_PT.json +22 -5
- umap/static/umap/locale/ro.js +22 -5
- umap/static/umap/locale/ro.json +22 -5
- umap/static/umap/locale/ru.js +22 -5
- umap/static/umap/locale/ru.json +22 -5
- umap/static/umap/locale/sk_SK.js +22 -5
- umap/static/umap/locale/sk_SK.json +22 -5
- umap/static/umap/locale/sl.js +22 -5
- umap/static/umap/locale/sl.json +22 -5
- umap/static/umap/locale/sr.js +22 -5
- umap/static/umap/locale/sr.json +22 -5
- umap/static/umap/locale/sv.js +22 -5
- umap/static/umap/locale/sv.json +22 -5
- umap/static/umap/locale/th_TH.js +22 -5
- umap/static/umap/locale/th_TH.json +22 -5
- umap/static/umap/locale/tr.js +22 -5
- umap/static/umap/locale/tr.json +22 -5
- umap/static/umap/locale/uk_UA.js +22 -5
- umap/static/umap/locale/uk_UA.json +22 -5
- umap/static/umap/locale/vi.js +22 -5
- umap/static/umap/locale/vi.json +22 -5
- umap/static/umap/locale/vi_VN.json +22 -5
- umap/static/umap/locale/zh.js +22 -5
- umap/static/umap/locale/zh.json +22 -5
- umap/static/umap/locale/zh_CN.json +22 -5
- umap/static/umap/locale/zh_TW.Big5.json +22 -5
- umap/static/umap/locale/zh_TW.js +22 -5
- umap/static/umap/locale/zh_TW.json +22 -5
- umap/static/umap/map.css +9 -153
- umap/static/umap/vars.css +15 -0
- umap/static/umap/vendors/dompurify/purify.es.js +5 -59
- umap/static/umap/vendors/dompurify/purify.es.mjs.map +1 -1
- umap/static/umap/vendors/formbuilder/Leaflet.FormBuilder.js +410 -428
- umap/static/umap/vendors/geojson-to-gpx/index.js +155 -0
- umap/static/umap/vendors/osmtogeojson/osmtogeojson.js +1 -2
- umap/static/umap/vendors/togeojson/togeojson.es.js +1109 -0
- umap/static/umap/vendors/togeojson/{togeojson.umd.js.map → togeojson.es.mjs.map} +1 -1
- umap/static/umap/vendors/tokml/tokml.es.js +895 -0
- umap/static/umap/vendors/tokml/tokml.es.mjs.map +1 -0
- umap/storage.py +6 -2
- umap/templates/umap/components/alerts/alert.html +3 -3
- umap/templates/umap/css.html +3 -0
- umap/templates/umap/js.html +0 -6
- umap/tests/fixtures/categorized_highway.geojson +1 -0
- umap/tests/fixtures/test_import_osm_relation.json +130 -0
- umap/tests/integration/conftest.py +8 -1
- umap/tests/integration/test_browser.py +3 -2
- umap/tests/integration/test_categorized_layer.py +141 -0
- umap/tests/integration/test_conditional_rules.py +21 -0
- umap/tests/integration/test_datalayer.py +9 -4
- umap/tests/integration/test_edit_datalayer.py +1 -0
- umap/tests/integration/test_edit_polygon.py +1 -1
- umap/tests/integration/test_export_map.py +2 -3
- umap/tests/integration/test_import.py +22 -0
- umap/tests/integration/test_map_preview.py +36 -2
- umap/tests/integration/test_tableeditor.py +158 -4
- umap/tests/integration/test_websocket_sync.py +2 -2
- umap/tests/test_views.py +2 -2
- umap/views.py +3 -2
- {umap_project-2.4.1.dist-info → umap_project-2.5.0.dist-info}/METADATA +8 -8
- {umap_project-2.4.1.dist-info → umap_project-2.5.0.dist-info}/RECORD +194 -184
- umap/static/umap/js/umap.slideshow.js +0 -165
- umap/static/umap/js/umap.tableeditor.js +0 -118
- umap/static/umap/vendors/togeojson/togeojson.umd.js +0 -2
- umap/static/umap/vendors/togpx/togpx.js +0 -547
- umap/static/umap/vendors/tokml/tokml.js +0 -343
- {umap_project-2.4.1.dist-info → umap_project-2.5.0.dist-info}/WHEEL +0 -0
- {umap_project-2.4.1.dist-info → umap_project-2.5.0.dist-info}/entry_points.txt +0 -0
- {umap_project-2.4.1.dist-info → umap_project-2.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,1109 @@
|
|
|
1
|
+
function $(element, tagName) {
|
|
2
|
+
return Array.from(element.getElementsByTagName(tagName));
|
|
3
|
+
}
|
|
4
|
+
function normalizeId(id) {
|
|
5
|
+
return id[0] === "#" ? id : `#${id}`;
|
|
6
|
+
}
|
|
7
|
+
function $ns(element, tagName, ns) {
|
|
8
|
+
return Array.from(element.getElementsByTagNameNS(ns, tagName));
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* get the content of a text node, if any
|
|
12
|
+
*/
|
|
13
|
+
function nodeVal(node) {
|
|
14
|
+
node?.normalize();
|
|
15
|
+
return (node && node.textContent) || "";
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Get one Y child of X, if any, otherwise null
|
|
19
|
+
*/
|
|
20
|
+
function get1(node, tagName, callback) {
|
|
21
|
+
const n = node.getElementsByTagName(tagName);
|
|
22
|
+
const result = n.length ? n[0] : null;
|
|
23
|
+
if (result && callback)
|
|
24
|
+
callback(result);
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
function get(node, tagName, callback) {
|
|
28
|
+
const properties = {};
|
|
29
|
+
if (!node)
|
|
30
|
+
return properties;
|
|
31
|
+
const n = node.getElementsByTagName(tagName);
|
|
32
|
+
const result = n.length ? n[0] : null;
|
|
33
|
+
if (result && callback) {
|
|
34
|
+
return callback(result, properties);
|
|
35
|
+
}
|
|
36
|
+
return properties;
|
|
37
|
+
}
|
|
38
|
+
function val1(node, tagName, callback) {
|
|
39
|
+
const val = nodeVal(get1(node, tagName));
|
|
40
|
+
if (val && callback)
|
|
41
|
+
return callback(val) || {};
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
function $num(node, tagName, callback) {
|
|
45
|
+
const val = parseFloat(nodeVal(get1(node, tagName)));
|
|
46
|
+
if (isNaN(val))
|
|
47
|
+
return undefined;
|
|
48
|
+
if (val && callback)
|
|
49
|
+
return callback(val) || {};
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
function num1(node, tagName, callback) {
|
|
53
|
+
const val = parseFloat(nodeVal(get1(node, tagName)));
|
|
54
|
+
if (isNaN(val))
|
|
55
|
+
return undefined;
|
|
56
|
+
if (callback)
|
|
57
|
+
callback(val);
|
|
58
|
+
return val;
|
|
59
|
+
}
|
|
60
|
+
function getMulti(node, propertyNames) {
|
|
61
|
+
const properties = {};
|
|
62
|
+
for (const property of propertyNames) {
|
|
63
|
+
val1(node, property, (val) => {
|
|
64
|
+
properties[property] = val;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return properties;
|
|
68
|
+
}
|
|
69
|
+
function isElement(node) {
|
|
70
|
+
return node?.nodeType === 1;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getLineStyle(node) {
|
|
74
|
+
return get(node, "line", (lineStyle) => {
|
|
75
|
+
const val = Object.assign({}, val1(lineStyle, "color", (color) => {
|
|
76
|
+
return { stroke: `#${color}` };
|
|
77
|
+
}), $num(lineStyle, "opacity", (opacity) => {
|
|
78
|
+
return { "stroke-opacity": opacity };
|
|
79
|
+
}), $num(lineStyle, "width", (width) => {
|
|
80
|
+
// GPX width is in mm, convert to px with 96 px per inch
|
|
81
|
+
return { "stroke-width": (width * 96) / 25.4 };
|
|
82
|
+
}));
|
|
83
|
+
return val;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getExtensions(node) {
|
|
88
|
+
let values = [];
|
|
89
|
+
if (node === null)
|
|
90
|
+
return values;
|
|
91
|
+
for (const child of Array.from(node.childNodes)) {
|
|
92
|
+
if (!isElement(child))
|
|
93
|
+
continue;
|
|
94
|
+
const name = abbreviateName(child.nodeName);
|
|
95
|
+
if (name === "gpxtpx:TrackPointExtension") {
|
|
96
|
+
// loop again for nested garmin extensions (eg. "gpxtpx:hr")
|
|
97
|
+
values = values.concat(getExtensions(child));
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
// push custom extension (eg. "power")
|
|
101
|
+
const val = nodeVal(child);
|
|
102
|
+
values.push([name, parseNumeric(val)]);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return values;
|
|
106
|
+
}
|
|
107
|
+
function abbreviateName(name) {
|
|
108
|
+
return ["heart", "gpxtpx:hr", "hr"].includes(name) ? "heart" : name;
|
|
109
|
+
}
|
|
110
|
+
function parseNumeric(val) {
|
|
111
|
+
const num = parseFloat(val);
|
|
112
|
+
return isNaN(num) ? val : num;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function coordPair$1(node) {
|
|
116
|
+
const ll = [
|
|
117
|
+
parseFloat(node.getAttribute("lon") || ""),
|
|
118
|
+
parseFloat(node.getAttribute("lat") || ""),
|
|
119
|
+
];
|
|
120
|
+
if (isNaN(ll[0]) || isNaN(ll[1])) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
num1(node, "ele", (val) => {
|
|
124
|
+
ll.push(val);
|
|
125
|
+
});
|
|
126
|
+
const time = get1(node, "time");
|
|
127
|
+
return {
|
|
128
|
+
coordinates: ll,
|
|
129
|
+
time: time ? nodeVal(time) : null,
|
|
130
|
+
extendedValues: getExtensions(get1(node, "extensions")),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function extractProperties(node) {
|
|
135
|
+
const properties = getMulti(node, [
|
|
136
|
+
"name",
|
|
137
|
+
"cmt",
|
|
138
|
+
"desc",
|
|
139
|
+
"type",
|
|
140
|
+
"time",
|
|
141
|
+
"keywords",
|
|
142
|
+
]);
|
|
143
|
+
const extensions = Array.from(node.getElementsByTagNameNS("http://www.garmin.com/xmlschemas/GpxExtensions/v3", "*"));
|
|
144
|
+
for (const child of extensions) {
|
|
145
|
+
if (child.parentNode?.parentNode === node) {
|
|
146
|
+
properties[child.tagName.replace(":", "_")] = nodeVal(child);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const links = $(node, "link");
|
|
150
|
+
if (links.length) {
|
|
151
|
+
properties.links = links.map((link) => Object.assign({ href: link.getAttribute("href") }, getMulti(link, ["text", "type"])));
|
|
152
|
+
}
|
|
153
|
+
return properties;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Extract points from a trkseg or rte element.
|
|
158
|
+
*/
|
|
159
|
+
function getPoints$1(node, pointname) {
|
|
160
|
+
const pts = $(node, pointname);
|
|
161
|
+
const line = [];
|
|
162
|
+
const times = [];
|
|
163
|
+
const extendedValues = {};
|
|
164
|
+
for (let i = 0; i < pts.length; i++) {
|
|
165
|
+
const c = coordPair$1(pts[i]);
|
|
166
|
+
if (!c) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
line.push(c.coordinates);
|
|
170
|
+
if (c.time)
|
|
171
|
+
times.push(c.time);
|
|
172
|
+
for (const [name, val] of c.extendedValues) {
|
|
173
|
+
const plural = name === "heart" ? name : name.replace("gpxtpx:", "") + "s";
|
|
174
|
+
if (!extendedValues[plural]) {
|
|
175
|
+
extendedValues[plural] = Array(pts.length).fill(null);
|
|
176
|
+
}
|
|
177
|
+
extendedValues[plural][i] = val;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (line.length < 2)
|
|
181
|
+
return; // Invalid line in GeoJSON
|
|
182
|
+
return {
|
|
183
|
+
line: line,
|
|
184
|
+
times: times,
|
|
185
|
+
extendedValues: extendedValues,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Extract a LineString geometry from a rte
|
|
190
|
+
* element.
|
|
191
|
+
*/
|
|
192
|
+
function getRoute(node) {
|
|
193
|
+
const line = getPoints$1(node, "rtept");
|
|
194
|
+
if (!line)
|
|
195
|
+
return;
|
|
196
|
+
return {
|
|
197
|
+
type: "Feature",
|
|
198
|
+
properties: Object.assign({ _gpxType: "rte" }, extractProperties(node), getLineStyle(get1(node, "extensions"))),
|
|
199
|
+
geometry: {
|
|
200
|
+
type: "LineString",
|
|
201
|
+
coordinates: line.line,
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function getTrack(node) {
|
|
206
|
+
const segments = $(node, "trkseg");
|
|
207
|
+
const track = [];
|
|
208
|
+
const times = [];
|
|
209
|
+
const extractedLines = [];
|
|
210
|
+
for (const segment of segments) {
|
|
211
|
+
const line = getPoints$1(segment, "trkpt");
|
|
212
|
+
if (line) {
|
|
213
|
+
extractedLines.push(line);
|
|
214
|
+
if (line.times && line.times.length)
|
|
215
|
+
times.push(line.times);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (extractedLines.length === 0)
|
|
219
|
+
return null;
|
|
220
|
+
const multi = extractedLines.length > 1;
|
|
221
|
+
const properties = Object.assign({ _gpxType: "trk" }, extractProperties(node), getLineStyle(get1(node, "extensions")), times.length
|
|
222
|
+
? {
|
|
223
|
+
coordinateProperties: {
|
|
224
|
+
times: multi ? times : times[0],
|
|
225
|
+
},
|
|
226
|
+
}
|
|
227
|
+
: {});
|
|
228
|
+
for (const line of extractedLines) {
|
|
229
|
+
track.push(line.line);
|
|
230
|
+
if (!properties.coordinateProperties) {
|
|
231
|
+
properties.coordinateProperties = {};
|
|
232
|
+
}
|
|
233
|
+
const props = properties.coordinateProperties;
|
|
234
|
+
const entries = Object.entries(line.extendedValues);
|
|
235
|
+
for (let i = 0; i < entries.length; i++) {
|
|
236
|
+
const [name, val] = entries[i];
|
|
237
|
+
if (multi) {
|
|
238
|
+
if (!props[name]) {
|
|
239
|
+
props[name] = extractedLines.map((line) => new Array(line.line.length).fill(null));
|
|
240
|
+
}
|
|
241
|
+
props[name][i] = val;
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
props[name] = val;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
type: "Feature",
|
|
250
|
+
properties: properties,
|
|
251
|
+
geometry: multi
|
|
252
|
+
? {
|
|
253
|
+
type: "MultiLineString",
|
|
254
|
+
coordinates: track,
|
|
255
|
+
}
|
|
256
|
+
: {
|
|
257
|
+
type: "LineString",
|
|
258
|
+
coordinates: track[0],
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Extract a point, if possible, from a given node,
|
|
264
|
+
* which is usually a wpt or trkpt
|
|
265
|
+
*/
|
|
266
|
+
function getPoint(node) {
|
|
267
|
+
const properties = Object.assign(extractProperties(node), getMulti(node, ["sym"]));
|
|
268
|
+
const pair = coordPair$1(node);
|
|
269
|
+
if (!pair)
|
|
270
|
+
return null;
|
|
271
|
+
return {
|
|
272
|
+
type: "Feature",
|
|
273
|
+
properties,
|
|
274
|
+
geometry: {
|
|
275
|
+
type: "Point",
|
|
276
|
+
coordinates: pair.coordinates,
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Convert GPX to GeoJSON incrementally, returning
|
|
282
|
+
* a [Generator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators)
|
|
283
|
+
* that yields output feature by feature.
|
|
284
|
+
*/
|
|
285
|
+
function* gpxGen(node) {
|
|
286
|
+
for (const track of $(node, "trk")) {
|
|
287
|
+
const feature = getTrack(track);
|
|
288
|
+
if (feature)
|
|
289
|
+
yield feature;
|
|
290
|
+
}
|
|
291
|
+
for (const route of $(node, "rte")) {
|
|
292
|
+
const feature = getRoute(route);
|
|
293
|
+
if (feature)
|
|
294
|
+
yield feature;
|
|
295
|
+
}
|
|
296
|
+
for (const waypoint of $(node, "wpt")) {
|
|
297
|
+
const point = getPoint(waypoint);
|
|
298
|
+
if (point)
|
|
299
|
+
yield point;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
*
|
|
304
|
+
* Convert a GPX document to GeoJSON. The first argument, `doc`, must be a GPX
|
|
305
|
+
* document as an XML DOM - not as a string. You can get this using jQuery's default
|
|
306
|
+
* `.ajax` function or using a bare XMLHttpRequest with the `.response` property
|
|
307
|
+
* holding an XML DOM.
|
|
308
|
+
*
|
|
309
|
+
* The output is a JavaScript object of GeoJSON data, same as `.kml` outputs, with the
|
|
310
|
+
* addition of a `_gpxType` property on each `LineString` feature that indicates whether
|
|
311
|
+
* the feature was encoded as a route (`rte`) or track (`trk`) in the GPX document.
|
|
312
|
+
*/
|
|
313
|
+
function gpx(node) {
|
|
314
|
+
return {
|
|
315
|
+
type: "FeatureCollection",
|
|
316
|
+
features: Array.from(gpxGen(node)),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const EXTENSIONS_NS = "http://www.garmin.com/xmlschemas/ActivityExtension/v2";
|
|
321
|
+
const TRACKPOINT_ATTRIBUTES = [
|
|
322
|
+
["heartRate", "heartRates"],
|
|
323
|
+
["Cadence", "cadences"],
|
|
324
|
+
// Extended Trackpoint attributes
|
|
325
|
+
["Speed", "speeds"],
|
|
326
|
+
["Watts", "watts"],
|
|
327
|
+
];
|
|
328
|
+
const LAP_ATTRIBUTES = [
|
|
329
|
+
["TotalTimeSeconds", "totalTimeSeconds"],
|
|
330
|
+
["DistanceMeters", "distanceMeters"],
|
|
331
|
+
["MaximumSpeed", "maxSpeed"],
|
|
332
|
+
["AverageHeartRateBpm", "avgHeartRate"],
|
|
333
|
+
["MaximumHeartRateBpm", "maxHeartRate"],
|
|
334
|
+
// Extended Lap attributes
|
|
335
|
+
["AvgSpeed", "avgSpeed"],
|
|
336
|
+
["AvgWatts", "avgWatts"],
|
|
337
|
+
["MaxWatts", "maxWatts"],
|
|
338
|
+
];
|
|
339
|
+
function getProperties(node, attributeNames) {
|
|
340
|
+
const properties = [];
|
|
341
|
+
for (const [tag, alias] of attributeNames) {
|
|
342
|
+
let elem = get1(node, tag);
|
|
343
|
+
if (!elem) {
|
|
344
|
+
const elements = node.getElementsByTagNameNS(EXTENSIONS_NS, tag);
|
|
345
|
+
if (elements.length) {
|
|
346
|
+
elem = elements[0];
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
const val = parseFloat(nodeVal(elem));
|
|
350
|
+
if (!isNaN(val)) {
|
|
351
|
+
properties.push([alias, val]);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return properties;
|
|
355
|
+
}
|
|
356
|
+
function coordPair(node) {
|
|
357
|
+
const ll = [num1(node, "LongitudeDegrees"), num1(node, "LatitudeDegrees")];
|
|
358
|
+
if (ll[0] === undefined ||
|
|
359
|
+
isNaN(ll[0]) ||
|
|
360
|
+
ll[1] === undefined ||
|
|
361
|
+
isNaN(ll[1])) {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
const heartRate = get1(node, "HeartRateBpm");
|
|
365
|
+
const time = nodeVal(get1(node, "Time"));
|
|
366
|
+
get1(node, "AltitudeMeters", (alt) => {
|
|
367
|
+
const a = parseFloat(nodeVal(alt));
|
|
368
|
+
if (!isNaN(a)) {
|
|
369
|
+
ll.push(a);
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
return {
|
|
373
|
+
coordinates: ll,
|
|
374
|
+
time: time || null,
|
|
375
|
+
heartRate: heartRate ? parseFloat(nodeVal(heartRate)) : null,
|
|
376
|
+
extensions: getProperties(node, TRACKPOINT_ATTRIBUTES),
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
function getPoints(node) {
|
|
380
|
+
const pts = $(node, "Trackpoint");
|
|
381
|
+
const line = [];
|
|
382
|
+
const times = [];
|
|
383
|
+
const heartRates = [];
|
|
384
|
+
if (pts.length < 2)
|
|
385
|
+
return null; // Invalid line in GeoJSON
|
|
386
|
+
const extendedProperties = {};
|
|
387
|
+
const result = { extendedProperties };
|
|
388
|
+
for (let i = 0; i < pts.length; i++) {
|
|
389
|
+
const c = coordPair(pts[i]);
|
|
390
|
+
if (c === null)
|
|
391
|
+
continue;
|
|
392
|
+
line.push(c.coordinates);
|
|
393
|
+
const { time, heartRate, extensions } = c;
|
|
394
|
+
if (time)
|
|
395
|
+
times.push(time);
|
|
396
|
+
if (heartRate)
|
|
397
|
+
heartRates.push(heartRate);
|
|
398
|
+
for (const [alias, value] of extensions) {
|
|
399
|
+
if (!extendedProperties[alias]) {
|
|
400
|
+
extendedProperties[alias] = Array(pts.length).fill(null);
|
|
401
|
+
}
|
|
402
|
+
extendedProperties[alias][i] = value;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (line.length < 2)
|
|
406
|
+
return null;
|
|
407
|
+
return Object.assign(result, {
|
|
408
|
+
line: line,
|
|
409
|
+
times: times,
|
|
410
|
+
heartRates: heartRates,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
function getLap(node) {
|
|
414
|
+
const segments = $(node, "Track");
|
|
415
|
+
const track = [];
|
|
416
|
+
const times = [];
|
|
417
|
+
const heartRates = [];
|
|
418
|
+
const allExtendedProperties = [];
|
|
419
|
+
let line;
|
|
420
|
+
const properties = Object.assign(Object.fromEntries(getProperties(node, LAP_ATTRIBUTES)), get(node, "Name", (nameElement) => {
|
|
421
|
+
return { name: nodeVal(nameElement) };
|
|
422
|
+
}));
|
|
423
|
+
for (const segment of segments) {
|
|
424
|
+
line = getPoints(segment);
|
|
425
|
+
if (line) {
|
|
426
|
+
track.push(line.line);
|
|
427
|
+
if (line.times.length)
|
|
428
|
+
times.push(line.times);
|
|
429
|
+
if (line.heartRates.length)
|
|
430
|
+
heartRates.push(line.heartRates);
|
|
431
|
+
allExtendedProperties.push(line.extendedProperties);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
for (let i = 0; i < allExtendedProperties.length; i++) {
|
|
435
|
+
const extendedProperties = allExtendedProperties[i];
|
|
436
|
+
for (const property in extendedProperties) {
|
|
437
|
+
if (segments.length === 1) {
|
|
438
|
+
if (line) {
|
|
439
|
+
properties[property] = line.extendedProperties[property];
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
if (!properties[property]) {
|
|
444
|
+
properties[property] = track.map((track) => Array(track.length).fill(null));
|
|
445
|
+
}
|
|
446
|
+
properties[property][i] = extendedProperties[property];
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (track.length === 0)
|
|
451
|
+
return null;
|
|
452
|
+
if (times.length || heartRates.length) {
|
|
453
|
+
properties.coordinateProperties = Object.assign(times.length
|
|
454
|
+
? {
|
|
455
|
+
times: track.length === 1 ? times[0] : times,
|
|
456
|
+
}
|
|
457
|
+
: {}, heartRates.length
|
|
458
|
+
? {
|
|
459
|
+
heart: track.length === 1 ? heartRates[0] : heartRates,
|
|
460
|
+
}
|
|
461
|
+
: {});
|
|
462
|
+
}
|
|
463
|
+
return {
|
|
464
|
+
type: "Feature",
|
|
465
|
+
properties: properties,
|
|
466
|
+
geometry: track.length === 1
|
|
467
|
+
? {
|
|
468
|
+
type: "LineString",
|
|
469
|
+
coordinates: track[0],
|
|
470
|
+
}
|
|
471
|
+
: {
|
|
472
|
+
type: "MultiLineString",
|
|
473
|
+
coordinates: track,
|
|
474
|
+
},
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Incrementally convert a TCX document to GeoJSON. The
|
|
479
|
+
* first argument, `doc`, must be a TCX
|
|
480
|
+
* document as an XML DOM - not as a string.
|
|
481
|
+
*/
|
|
482
|
+
function* tcxGen(node) {
|
|
483
|
+
for (const lap of $(node, "Lap")) {
|
|
484
|
+
const feature = getLap(lap);
|
|
485
|
+
if (feature)
|
|
486
|
+
yield feature;
|
|
487
|
+
}
|
|
488
|
+
for (const course of $(node, "Courses")) {
|
|
489
|
+
const feature = getLap(course);
|
|
490
|
+
if (feature)
|
|
491
|
+
yield feature;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Convert a TCX document to GeoJSON. The first argument, `doc`, must be a TCX
|
|
496
|
+
* document as an XML DOM - not as a string.
|
|
497
|
+
*/
|
|
498
|
+
function tcx(node) {
|
|
499
|
+
return {
|
|
500
|
+
type: "FeatureCollection",
|
|
501
|
+
features: Array.from(tcxGen(node)),
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function fixColor(v, prefix) {
|
|
506
|
+
const properties = {};
|
|
507
|
+
const colorProp = prefix == "stroke" || prefix === "fill" ? prefix : prefix + "-color";
|
|
508
|
+
if (v[0] === "#") {
|
|
509
|
+
v = v.substring(1);
|
|
510
|
+
}
|
|
511
|
+
if (v.length === 6 || v.length === 3) {
|
|
512
|
+
properties[colorProp] = "#" + v;
|
|
513
|
+
}
|
|
514
|
+
else if (v.length === 8) {
|
|
515
|
+
properties[prefix + "-opacity"] = parseInt(v.substring(0, 2), 16) / 255;
|
|
516
|
+
properties[colorProp] =
|
|
517
|
+
"#" + v.substring(6, 8) + v.substring(4, 6) + v.substring(2, 4);
|
|
518
|
+
}
|
|
519
|
+
return properties;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function numericProperty(node, source, target) {
|
|
523
|
+
const properties = {};
|
|
524
|
+
num1(node, source, (val) => {
|
|
525
|
+
properties[target] = val;
|
|
526
|
+
});
|
|
527
|
+
return properties;
|
|
528
|
+
}
|
|
529
|
+
function getColor(node, output) {
|
|
530
|
+
return get(node, "color", (elem) => fixColor(nodeVal(elem), output));
|
|
531
|
+
}
|
|
532
|
+
function extractIconHref(node) {
|
|
533
|
+
return get(node, "Icon", (icon, properties) => {
|
|
534
|
+
val1(icon, "href", (href) => {
|
|
535
|
+
properties.icon = href;
|
|
536
|
+
});
|
|
537
|
+
return properties;
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
function extractIcon(node) {
|
|
541
|
+
return get(node, "IconStyle", (iconStyle) => {
|
|
542
|
+
return Object.assign(getColor(iconStyle, "icon"), numericProperty(iconStyle, "scale", "icon-scale"), numericProperty(iconStyle, "heading", "icon-heading"), get(iconStyle, "hotSpot", (hotspot) => {
|
|
543
|
+
const left = parseFloat(hotspot.getAttribute("x") || "");
|
|
544
|
+
const top = parseFloat(hotspot.getAttribute("y") || "");
|
|
545
|
+
const xunits = hotspot.getAttribute("xunits") || "";
|
|
546
|
+
const yunits = hotspot.getAttribute("yunits") || "";
|
|
547
|
+
if (!isNaN(left) && !isNaN(top))
|
|
548
|
+
return {
|
|
549
|
+
"icon-offset": [left, top],
|
|
550
|
+
"icon-offset-units": [xunits, yunits],
|
|
551
|
+
};
|
|
552
|
+
return {};
|
|
553
|
+
}), extractIconHref(iconStyle));
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
function extractLabel(node) {
|
|
557
|
+
return get(node, "LabelStyle", (labelStyle) => {
|
|
558
|
+
return Object.assign(getColor(labelStyle, "label"), numericProperty(labelStyle, "scale", "label-scale"));
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
function extractLine(node) {
|
|
562
|
+
return get(node, "LineStyle", (lineStyle) => {
|
|
563
|
+
return Object.assign(getColor(lineStyle, "stroke"), numericProperty(lineStyle, "width", "stroke-width"));
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
function extractPoly(node) {
|
|
567
|
+
return get(node, "PolyStyle", (polyStyle, properties) => {
|
|
568
|
+
return Object.assign(properties, get(polyStyle, "color", (elem) => fixColor(nodeVal(elem), "fill")), val1(polyStyle, "fill", (fill) => {
|
|
569
|
+
if (fill === "0")
|
|
570
|
+
return { "fill-opacity": 0 };
|
|
571
|
+
}), val1(polyStyle, "outline", (outline) => {
|
|
572
|
+
if (outline === "0")
|
|
573
|
+
return { "stroke-opacity": 0 };
|
|
574
|
+
}));
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
function extractStyle(node) {
|
|
578
|
+
return Object.assign({}, extractPoly(node), extractLine(node), extractLabel(node), extractIcon(node));
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const toNumber = (x) => Number(x);
|
|
582
|
+
const typeConverters = {
|
|
583
|
+
string: (x) => x,
|
|
584
|
+
int: toNumber,
|
|
585
|
+
uint: toNumber,
|
|
586
|
+
short: toNumber,
|
|
587
|
+
ushort: toNumber,
|
|
588
|
+
float: toNumber,
|
|
589
|
+
double: toNumber,
|
|
590
|
+
bool: (x) => Boolean(x),
|
|
591
|
+
};
|
|
592
|
+
function extractExtendedData(node, schema) {
|
|
593
|
+
return get(node, "ExtendedData", (extendedData, properties) => {
|
|
594
|
+
for (const data of $(extendedData, "Data")) {
|
|
595
|
+
properties[data.getAttribute("name") || ""] = nodeVal(get1(data, "value"));
|
|
596
|
+
}
|
|
597
|
+
for (const simpleData of $(extendedData, "SimpleData")) {
|
|
598
|
+
const name = simpleData.getAttribute("name") || "";
|
|
599
|
+
const typeConverter = schema[name] || typeConverters.string;
|
|
600
|
+
properties[name] = typeConverter(nodeVal(simpleData));
|
|
601
|
+
}
|
|
602
|
+
return properties;
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
function getMaybeHTMLDescription(node) {
|
|
606
|
+
const descriptionNode = get1(node, "description");
|
|
607
|
+
for (const c of Array.from(descriptionNode?.childNodes || [])) {
|
|
608
|
+
if (c.nodeType === 4) {
|
|
609
|
+
return {
|
|
610
|
+
description: {
|
|
611
|
+
"@type": "html",
|
|
612
|
+
value: nodeVal(c),
|
|
613
|
+
},
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return {};
|
|
618
|
+
}
|
|
619
|
+
function extractTimeSpan(node) {
|
|
620
|
+
return get(node, "TimeSpan", (timeSpan) => {
|
|
621
|
+
return {
|
|
622
|
+
timespan: {
|
|
623
|
+
begin: nodeVal(get1(timeSpan, "begin")),
|
|
624
|
+
end: nodeVal(get1(timeSpan, "end")),
|
|
625
|
+
},
|
|
626
|
+
};
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
function extractTimeStamp(node) {
|
|
630
|
+
return get(node, "TimeStamp", (timeStamp) => {
|
|
631
|
+
return { timestamp: nodeVal(get1(timeStamp, "when")) };
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
function extractCascadedStyle(node, styleMap) {
|
|
635
|
+
return val1(node, "styleUrl", (styleUrl) => {
|
|
636
|
+
styleUrl = normalizeId(styleUrl);
|
|
637
|
+
if (styleMap[styleUrl]) {
|
|
638
|
+
return Object.assign({ styleUrl }, styleMap[styleUrl]);
|
|
639
|
+
}
|
|
640
|
+
// For backward-compatibility. Should we still include
|
|
641
|
+
// styleUrl even if it's not resolved?
|
|
642
|
+
return { styleUrl };
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const removeSpace = /\s*/g;
|
|
647
|
+
const trimSpace = /^\s*|\s*$/g;
|
|
648
|
+
const splitSpace = /\s+/;
|
|
649
|
+
/**
|
|
650
|
+
* Get one coordinate from a coordinate array, if any
|
|
651
|
+
*/
|
|
652
|
+
function coord1(value) {
|
|
653
|
+
return value
|
|
654
|
+
.replace(removeSpace, "")
|
|
655
|
+
.split(",")
|
|
656
|
+
.map(parseFloat)
|
|
657
|
+
.filter((num) => !isNaN(num))
|
|
658
|
+
.slice(0, 3);
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Get all coordinates from a coordinate array as [[],[]]
|
|
662
|
+
*/
|
|
663
|
+
function coord(value) {
|
|
664
|
+
return value
|
|
665
|
+
.replace(trimSpace, "")
|
|
666
|
+
.split(splitSpace)
|
|
667
|
+
.map(coord1)
|
|
668
|
+
.filter((coord) => {
|
|
669
|
+
return coord.length >= 2;
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
function gxCoords(node) {
|
|
673
|
+
let elems = $(node, "coord");
|
|
674
|
+
if (elems.length === 0) {
|
|
675
|
+
elems = $ns(node, "coord", "*");
|
|
676
|
+
}
|
|
677
|
+
const coordinates = elems.map((elem) => {
|
|
678
|
+
return nodeVal(elem).split(" ").map(parseFloat);
|
|
679
|
+
});
|
|
680
|
+
if (coordinates.length === 0) {
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
683
|
+
return {
|
|
684
|
+
geometry: coordinates.length > 2
|
|
685
|
+
? {
|
|
686
|
+
type: "LineString",
|
|
687
|
+
coordinates,
|
|
688
|
+
}
|
|
689
|
+
: {
|
|
690
|
+
type: "Point",
|
|
691
|
+
coordinates: coordinates[0],
|
|
692
|
+
},
|
|
693
|
+
times: $(node, "when").map((elem) => nodeVal(elem)),
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
function fixRing(ring) {
|
|
697
|
+
if (ring.length === 0)
|
|
698
|
+
return ring;
|
|
699
|
+
const first = ring[0];
|
|
700
|
+
const last = ring[ring.length - 1];
|
|
701
|
+
let equal = true;
|
|
702
|
+
for (let i = 0; i < Math.max(first.length, last.length); i++) {
|
|
703
|
+
if (first[i] !== last[i]) {
|
|
704
|
+
equal = false;
|
|
705
|
+
break;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
if (!equal) {
|
|
709
|
+
return ring.concat([ring[0]]);
|
|
710
|
+
}
|
|
711
|
+
return ring;
|
|
712
|
+
}
|
|
713
|
+
function getCoordinates(node) {
|
|
714
|
+
return nodeVal(get1(node, "coordinates"));
|
|
715
|
+
}
|
|
716
|
+
function getGeometry(node) {
|
|
717
|
+
let geometries = [];
|
|
718
|
+
let coordTimes = [];
|
|
719
|
+
for (let i = 0; i < node.childNodes.length; i++) {
|
|
720
|
+
const child = node.childNodes.item(i);
|
|
721
|
+
if (isElement(child)) {
|
|
722
|
+
switch (child.tagName) {
|
|
723
|
+
case "MultiGeometry":
|
|
724
|
+
case "MultiTrack":
|
|
725
|
+
case "gx:MultiTrack": {
|
|
726
|
+
const childGeometries = getGeometry(child);
|
|
727
|
+
geometries = geometries.concat(childGeometries.geometries);
|
|
728
|
+
coordTimes = coordTimes.concat(childGeometries.coordTimes);
|
|
729
|
+
break;
|
|
730
|
+
}
|
|
731
|
+
case "Point": {
|
|
732
|
+
const coordinates = coord1(getCoordinates(child));
|
|
733
|
+
if (coordinates.length >= 2) {
|
|
734
|
+
geometries.push({
|
|
735
|
+
type: "Point",
|
|
736
|
+
coordinates,
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
break;
|
|
740
|
+
}
|
|
741
|
+
case "LinearRing":
|
|
742
|
+
case "LineString": {
|
|
743
|
+
const coordinates = coord(getCoordinates(child));
|
|
744
|
+
if (coordinates.length >= 2) {
|
|
745
|
+
geometries.push({
|
|
746
|
+
type: "LineString",
|
|
747
|
+
coordinates,
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
break;
|
|
751
|
+
}
|
|
752
|
+
case "Polygon": {
|
|
753
|
+
const coords = [];
|
|
754
|
+
for (const linearRing of $(child, "LinearRing")) {
|
|
755
|
+
const ring = fixRing(coord(getCoordinates(linearRing)));
|
|
756
|
+
if (ring.length >= 4) {
|
|
757
|
+
coords.push(ring);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
if (coords.length) {
|
|
761
|
+
geometries.push({
|
|
762
|
+
type: "Polygon",
|
|
763
|
+
coordinates: coords,
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
break;
|
|
767
|
+
}
|
|
768
|
+
case "Track":
|
|
769
|
+
case "gx:Track": {
|
|
770
|
+
const gx = gxCoords(child);
|
|
771
|
+
if (!gx)
|
|
772
|
+
break;
|
|
773
|
+
const { times, geometry } = gx;
|
|
774
|
+
geometries.push(geometry);
|
|
775
|
+
if (times.length)
|
|
776
|
+
coordTimes.push(times);
|
|
777
|
+
break;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
return {
|
|
783
|
+
geometries,
|
|
784
|
+
coordTimes,
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function geometryListToGeometry(geometries) {
|
|
789
|
+
return geometries.length === 0
|
|
790
|
+
? null
|
|
791
|
+
: geometries.length === 1
|
|
792
|
+
? geometries[0]
|
|
793
|
+
: {
|
|
794
|
+
type: "GeometryCollection",
|
|
795
|
+
geometries,
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
function getPlacemark(node, styleMap, schema, options) {
|
|
799
|
+
const { coordTimes, geometries } = getGeometry(node);
|
|
800
|
+
const geometry = geometryListToGeometry(geometries);
|
|
801
|
+
if (!geometry && options.skipNullGeometry) {
|
|
802
|
+
return null;
|
|
803
|
+
}
|
|
804
|
+
const feature = {
|
|
805
|
+
type: "Feature",
|
|
806
|
+
geometry,
|
|
807
|
+
properties: Object.assign(getMulti(node, [
|
|
808
|
+
"name",
|
|
809
|
+
"address",
|
|
810
|
+
"visibility",
|
|
811
|
+
"open",
|
|
812
|
+
"phoneNumber",
|
|
813
|
+
"description",
|
|
814
|
+
]), getMaybeHTMLDescription(node), extractCascadedStyle(node, styleMap), extractStyle(node), extractExtendedData(node, schema), extractTimeSpan(node), extractTimeStamp(node), coordTimes.length
|
|
815
|
+
? {
|
|
816
|
+
coordinateProperties: {
|
|
817
|
+
times: coordTimes.length === 1 ? coordTimes[0] : coordTimes,
|
|
818
|
+
},
|
|
819
|
+
}
|
|
820
|
+
: {}),
|
|
821
|
+
};
|
|
822
|
+
if (feature.properties?.visibility !== undefined) {
|
|
823
|
+
feature.properties.visibility = feature.properties.visibility !== "0";
|
|
824
|
+
}
|
|
825
|
+
const id = node.getAttribute("id");
|
|
826
|
+
if (id !== null && id !== "")
|
|
827
|
+
feature.id = id;
|
|
828
|
+
return feature;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function getGroundOverlayBox(node) {
|
|
832
|
+
const latLonQuad = get1(node, "gx:LatLonQuad");
|
|
833
|
+
if (latLonQuad) {
|
|
834
|
+
const ring = fixRing(coord(getCoordinates(node)));
|
|
835
|
+
return {
|
|
836
|
+
geometry: {
|
|
837
|
+
type: "Polygon",
|
|
838
|
+
coordinates: [ring],
|
|
839
|
+
},
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
return getLatLonBox(node);
|
|
843
|
+
}
|
|
844
|
+
const DEGREES_TO_RADIANS = Math.PI / 180;
|
|
845
|
+
function rotateBox(bbox, coordinates, rotation) {
|
|
846
|
+
const center = [(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2];
|
|
847
|
+
return [
|
|
848
|
+
coordinates[0].map((coordinate) => {
|
|
849
|
+
const dy = coordinate[1] - center[1];
|
|
850
|
+
const dx = coordinate[0] - center[0];
|
|
851
|
+
const distance = Math.sqrt(Math.pow(dy, 2) + Math.pow(dx, 2));
|
|
852
|
+
const angle = Math.atan2(dy, dx) + rotation * DEGREES_TO_RADIANS;
|
|
853
|
+
return [
|
|
854
|
+
center[0] + Math.cos(angle) * distance,
|
|
855
|
+
center[1] + Math.sin(angle) * distance,
|
|
856
|
+
];
|
|
857
|
+
}),
|
|
858
|
+
];
|
|
859
|
+
}
|
|
860
|
+
function getLatLonBox(node) {
|
|
861
|
+
const latLonBox = get1(node, "LatLonBox");
|
|
862
|
+
if (latLonBox) {
|
|
863
|
+
const north = num1(latLonBox, "north");
|
|
864
|
+
const west = num1(latLonBox, "west");
|
|
865
|
+
const east = num1(latLonBox, "east");
|
|
866
|
+
const south = num1(latLonBox, "south");
|
|
867
|
+
const rotation = num1(latLonBox, "rotation");
|
|
868
|
+
if (typeof north === "number" &&
|
|
869
|
+
typeof south === "number" &&
|
|
870
|
+
typeof west === "number" &&
|
|
871
|
+
typeof east === "number") {
|
|
872
|
+
const bbox = [west, south, east, north];
|
|
873
|
+
let coordinates = [
|
|
874
|
+
[
|
|
875
|
+
[west, north],
|
|
876
|
+
[east, north],
|
|
877
|
+
[east, south],
|
|
878
|
+
[west, south],
|
|
879
|
+
[west, north], // top left (again)
|
|
880
|
+
],
|
|
881
|
+
];
|
|
882
|
+
if (typeof rotation === "number") {
|
|
883
|
+
coordinates = rotateBox(bbox, coordinates, rotation);
|
|
884
|
+
}
|
|
885
|
+
return {
|
|
886
|
+
bbox,
|
|
887
|
+
geometry: {
|
|
888
|
+
type: "Polygon",
|
|
889
|
+
coordinates,
|
|
890
|
+
},
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
return null;
|
|
895
|
+
}
|
|
896
|
+
function getGroundOverlay(node, styleMap, schema, options) {
|
|
897
|
+
const box = getGroundOverlayBox(node);
|
|
898
|
+
const geometry = box?.geometry || null;
|
|
899
|
+
if (!geometry && options.skipNullGeometry) {
|
|
900
|
+
return null;
|
|
901
|
+
}
|
|
902
|
+
const feature = {
|
|
903
|
+
type: "Feature",
|
|
904
|
+
geometry,
|
|
905
|
+
properties: Object.assign(
|
|
906
|
+
/**
|
|
907
|
+
* Related to
|
|
908
|
+
* https://gist.github.com/tmcw/037a1cb6660d74a392e9da7446540f46
|
|
909
|
+
*/
|
|
910
|
+
{ "@geometry-type": "groundoverlay" }, getMulti(node, [
|
|
911
|
+
"name",
|
|
912
|
+
"address",
|
|
913
|
+
"visibility",
|
|
914
|
+
"open",
|
|
915
|
+
"phoneNumber",
|
|
916
|
+
"description",
|
|
917
|
+
]), getMaybeHTMLDescription(node), extractCascadedStyle(node, styleMap), extractStyle(node), extractIconHref(node), extractExtendedData(node, schema), extractTimeSpan(node), extractTimeStamp(node)),
|
|
918
|
+
};
|
|
919
|
+
if (box?.bbox) {
|
|
920
|
+
feature.bbox = box.bbox;
|
|
921
|
+
}
|
|
922
|
+
if (feature.properties?.visibility !== undefined) {
|
|
923
|
+
feature.properties.visibility = feature.properties.visibility !== "0";
|
|
924
|
+
}
|
|
925
|
+
const id = node.getAttribute("id");
|
|
926
|
+
if (id !== null && id !== "")
|
|
927
|
+
feature.id = id;
|
|
928
|
+
return feature;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function getStyleId(style) {
|
|
932
|
+
let id = style.getAttribute("id");
|
|
933
|
+
const parentNode = style.parentNode;
|
|
934
|
+
if (!id &&
|
|
935
|
+
isElement(parentNode) &&
|
|
936
|
+
parentNode.localName === "CascadingStyle") {
|
|
937
|
+
id = parentNode.getAttribute("kml:id") || parentNode.getAttribute("id");
|
|
938
|
+
}
|
|
939
|
+
return normalizeId(id || "");
|
|
940
|
+
}
|
|
941
|
+
function buildStyleMap(node) {
|
|
942
|
+
const styleMap = {};
|
|
943
|
+
for (const style of $(node, "Style")) {
|
|
944
|
+
styleMap[getStyleId(style)] = extractStyle(style);
|
|
945
|
+
}
|
|
946
|
+
for (const map of $(node, "StyleMap")) {
|
|
947
|
+
const id = normalizeId(map.getAttribute("id") || "");
|
|
948
|
+
val1(map, "styleUrl", (styleUrl) => {
|
|
949
|
+
styleUrl = normalizeId(styleUrl);
|
|
950
|
+
if (styleMap[styleUrl]) {
|
|
951
|
+
styleMap[id] = styleMap[styleUrl];
|
|
952
|
+
}
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
return styleMap;
|
|
956
|
+
}
|
|
957
|
+
function buildSchema(node) {
|
|
958
|
+
const schema = {};
|
|
959
|
+
for (const field of $(node, "SimpleField")) {
|
|
960
|
+
schema[field.getAttribute("name") || ""] =
|
|
961
|
+
typeConverters[field.getAttribute("type") || ""] ||
|
|
962
|
+
typeConverters["string"];
|
|
963
|
+
}
|
|
964
|
+
return schema;
|
|
965
|
+
}
|
|
966
|
+
const FOLDER_PROPS = [
|
|
967
|
+
"name",
|
|
968
|
+
"visibility",
|
|
969
|
+
"open",
|
|
970
|
+
"address",
|
|
971
|
+
"description",
|
|
972
|
+
"phoneNumber",
|
|
973
|
+
"visibility",
|
|
974
|
+
];
|
|
975
|
+
function getFolder(node) {
|
|
976
|
+
const meta = {};
|
|
977
|
+
for (const child of Array.from(node.childNodes)) {
|
|
978
|
+
if (isElement(child) && FOLDER_PROPS.includes(child.tagName)) {
|
|
979
|
+
meta[child.tagName] = nodeVal(child);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
return {
|
|
983
|
+
type: "folder",
|
|
984
|
+
meta,
|
|
985
|
+
children: [],
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Yield a nested tree with KML folder structure
|
|
990
|
+
*
|
|
991
|
+
* This generates a tree with the given structure:
|
|
992
|
+
*
|
|
993
|
+
* ```js
|
|
994
|
+
* {
|
|
995
|
+
* "type": "root",
|
|
996
|
+
* "children": [
|
|
997
|
+
* {
|
|
998
|
+
* "type": "folder",
|
|
999
|
+
* "meta": {
|
|
1000
|
+
* "name": "Test"
|
|
1001
|
+
* },
|
|
1002
|
+
* "children": [
|
|
1003
|
+
* // ...features and folders
|
|
1004
|
+
* ]
|
|
1005
|
+
* }
|
|
1006
|
+
* // ...features
|
|
1007
|
+
* ]
|
|
1008
|
+
* }
|
|
1009
|
+
* ```
|
|
1010
|
+
*
|
|
1011
|
+
* ### GroundOverlay
|
|
1012
|
+
*
|
|
1013
|
+
* GroundOverlay elements are converted into
|
|
1014
|
+
* `Feature` objects with `Polygon` geometries,
|
|
1015
|
+
* a property like:
|
|
1016
|
+
*
|
|
1017
|
+
* ```json
|
|
1018
|
+
* {
|
|
1019
|
+
* "@geometry-type": "groundoverlay"
|
|
1020
|
+
* }
|
|
1021
|
+
* ```
|
|
1022
|
+
*
|
|
1023
|
+
* And the ground overlay's image URL in the `href`
|
|
1024
|
+
* property. Ground overlays will need to be displayed
|
|
1025
|
+
* with a separate method to other features, depending
|
|
1026
|
+
* on which map framework you're using.
|
|
1027
|
+
*/
|
|
1028
|
+
function kmlWithFolders(node, options = {
|
|
1029
|
+
skipNullGeometry: false,
|
|
1030
|
+
}) {
|
|
1031
|
+
const styleMap = buildStyleMap(node);
|
|
1032
|
+
const schema = buildSchema(node);
|
|
1033
|
+
const tree = { type: "root", children: [] };
|
|
1034
|
+
function traverse(node, pointer, options) {
|
|
1035
|
+
if (isElement(node)) {
|
|
1036
|
+
switch (node.tagName) {
|
|
1037
|
+
case "GroundOverlay": {
|
|
1038
|
+
const placemark = getGroundOverlay(node, styleMap, schema, options);
|
|
1039
|
+
if (placemark) {
|
|
1040
|
+
pointer.children.push(placemark);
|
|
1041
|
+
}
|
|
1042
|
+
break;
|
|
1043
|
+
}
|
|
1044
|
+
case "Placemark": {
|
|
1045
|
+
const placemark = getPlacemark(node, styleMap, schema, options);
|
|
1046
|
+
if (placemark) {
|
|
1047
|
+
pointer.children.push(placemark);
|
|
1048
|
+
}
|
|
1049
|
+
break;
|
|
1050
|
+
}
|
|
1051
|
+
case "Folder": {
|
|
1052
|
+
const folder = getFolder(node);
|
|
1053
|
+
pointer.children.push(folder);
|
|
1054
|
+
pointer = folder;
|
|
1055
|
+
break;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
if (node.childNodes) {
|
|
1060
|
+
for (let i = 0; i < node.childNodes.length; i++) {
|
|
1061
|
+
traverse(node.childNodes[i], pointer, options);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
traverse(node, tree, options);
|
|
1066
|
+
return tree;
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
* Convert KML to GeoJSON incrementally, returning
|
|
1070
|
+
* a [Generator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators)
|
|
1071
|
+
* that yields output feature by feature.
|
|
1072
|
+
*/
|
|
1073
|
+
function* kmlGen(node, options = {
|
|
1074
|
+
skipNullGeometry: false,
|
|
1075
|
+
}) {
|
|
1076
|
+
const styleMap = buildStyleMap(node);
|
|
1077
|
+
const schema = buildSchema(node);
|
|
1078
|
+
for (const placemark of $(node, "Placemark")) {
|
|
1079
|
+
const feature = getPlacemark(placemark, styleMap, schema, options);
|
|
1080
|
+
if (feature)
|
|
1081
|
+
yield feature;
|
|
1082
|
+
}
|
|
1083
|
+
for (const groundOverlay of $(node, "GroundOverlay")) {
|
|
1084
|
+
const feature = getGroundOverlay(groundOverlay, styleMap, schema, options);
|
|
1085
|
+
if (feature)
|
|
1086
|
+
yield feature;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* Convert a KML document to GeoJSON. The first argument, `doc`, must be a KML
|
|
1091
|
+
* document as an XML DOM - not as a string. You can get this using jQuery's default
|
|
1092
|
+
* `.ajax` function or using a bare XMLHttpRequest with the `.response` property
|
|
1093
|
+
* holding an XML DOM.
|
|
1094
|
+
*
|
|
1095
|
+
* The output is a JavaScript object of GeoJSON data. You can convert it to a string
|
|
1096
|
+
* with [JSON.stringify](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify)
|
|
1097
|
+
* or use it directly in libraries.
|
|
1098
|
+
*/
|
|
1099
|
+
function kml(node, options = {
|
|
1100
|
+
skipNullGeometry: false,
|
|
1101
|
+
}) {
|
|
1102
|
+
return {
|
|
1103
|
+
type: "FeatureCollection",
|
|
1104
|
+
features: Array.from(kmlGen(node, options)),
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
export { gpx, gpxGen, kml, kmlGen, kmlWithFolders, tcx, tcxGen };
|
|
1109
|
+
//# sourceMappingURL=togeojson.es.mjs.map
|