GpxDataSource.js 30 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021
  1. import Cartesian2 from "../Core/Cartesian2.js";
  2. import Cartesian3 from "../Core/Cartesian3.js";
  3. import ClockRange from "../Core/ClockRange.js";
  4. import ClockStep from "../Core/ClockStep.js";
  5. import Color from "../Core/Color.js";
  6. import createGuid from "../Core/createGuid.js";
  7. import defaultValue from "../Core/defaultValue.js";
  8. import defined from "../Core/defined.js";
  9. import DeveloperError from "../Core/DeveloperError.js";
  10. import Event from "../Core/Event.js";
  11. import Iso8601 from "../Core/Iso8601.js";
  12. import JulianDate from "../Core/JulianDate.js";
  13. import NearFarScalar from "../Core/NearFarScalar.js";
  14. import PinBuilder from "../Core/PinBuilder.js";
  15. import Resource from "../Core/Resource.js";
  16. import RuntimeError from "../Core/RuntimeError.js";
  17. import TimeInterval from "../Core/TimeInterval.js";
  18. import TimeIntervalCollection from "../Core/TimeIntervalCollection.js";
  19. import HeightReference from "../Scene/HeightReference.js";
  20. import HorizontalOrigin from "../Scene/HorizontalOrigin.js";
  21. import LabelStyle from "../Scene/LabelStyle.js";
  22. import VerticalOrigin from "../Scene/VerticalOrigin.js";
  23. import Autolinker from "autolinker";
  24. import BillboardGraphics from "./BillboardGraphics.js";
  25. import ConstantProperty from "./ConstantProperty.js";
  26. import DataSource from "./DataSource.js";
  27. import DataSourceClock from "./DataSourceClock.js";
  28. import EntityCluster from "./EntityCluster.js";
  29. import EntityCollection from "./EntityCollection.js";
  30. import LabelGraphics from "./LabelGraphics.js";
  31. import PolylineGraphics from "./PolylineGraphics.js";
  32. import PolylineOutlineMaterialProperty from "./PolylineOutlineMaterialProperty.js";
  33. import SampledPositionProperty from "./SampledPositionProperty.js";
  34. let parser;
  35. if (typeof DOMParser !== "undefined") {
  36. parser = new DOMParser();
  37. }
  38. const autolinker = new Autolinker({
  39. stripPrefix: false,
  40. email: false,
  41. replaceFn: function (linker, match) {
  42. //Prevent matching of non-explicit urls.
  43. //i.e. foo.id won't match but http://foo.id will
  44. return match.urlMatchType === "scheme" || match.urlMatchType === "www";
  45. },
  46. });
  47. const BILLBOARD_SIZE = 32;
  48. const BILLBOARD_NEAR_DISTANCE = 2414016;
  49. const BILLBOARD_NEAR_RATIO = 1.0;
  50. const BILLBOARD_FAR_DISTANCE = 1.6093e7;
  51. const BILLBOARD_FAR_RATIO = 0.1;
  52. const gpxNamespaces = [null, undefined, "http://www.topografix.com/GPX/1/1"];
  53. const namespaces = {
  54. gpx: gpxNamespaces,
  55. };
  56. function readBlobAsText(blob) {
  57. return new Promise((resolve, reject) => {
  58. const reader = new FileReader();
  59. reader.addEventListener("load", function () {
  60. resolve(reader.result);
  61. });
  62. reader.addEventListener("error", function () {
  63. reject(reader.error);
  64. });
  65. reader.readAsText(blob);
  66. });
  67. }
  68. function getOrCreateEntity(node, entityCollection) {
  69. let id = queryStringAttribute(node, "id");
  70. id = defined(id) ? id : createGuid();
  71. const entity = entityCollection.getOrCreateEntity(id);
  72. return entity;
  73. }
  74. function readCoordinateFromNode(node) {
  75. const longitude = queryNumericAttribute(node, "lon");
  76. const latitude = queryNumericAttribute(node, "lat");
  77. const elevation = queryNumericValue(node, "ele", namespaces.gpx);
  78. return Cartesian3.fromDegrees(longitude, latitude, elevation);
  79. }
  80. function queryNumericAttribute(node, attributeName) {
  81. if (!defined(node)) {
  82. return undefined;
  83. }
  84. const value = node.getAttribute(attributeName);
  85. if (value !== null) {
  86. const result = parseFloat(value);
  87. return !isNaN(result) ? result : undefined;
  88. }
  89. return undefined;
  90. }
  91. function queryStringAttribute(node, attributeName) {
  92. if (!defined(node)) {
  93. return undefined;
  94. }
  95. const value = node.getAttribute(attributeName);
  96. return value !== null ? value : undefined;
  97. }
  98. function queryFirstNode(node, tagName, namespace) {
  99. if (!defined(node)) {
  100. return undefined;
  101. }
  102. const childNodes = node.childNodes;
  103. const length = childNodes.length;
  104. for (let q = 0; q < length; q++) {
  105. const child = childNodes[q];
  106. if (
  107. child.localName === tagName &&
  108. namespace.indexOf(child.namespaceURI) !== -1
  109. ) {
  110. return child;
  111. }
  112. }
  113. return undefined;
  114. }
  115. function queryNodes(node, tagName, namespace) {
  116. if (!defined(node)) {
  117. return undefined;
  118. }
  119. const result = [];
  120. const childNodes = node.getElementsByTagName(tagName);
  121. const length = childNodes.length;
  122. for (let q = 0; q < length; q++) {
  123. const child = childNodes[q];
  124. if (
  125. child.localName === tagName &&
  126. namespace.indexOf(child.namespaceURI) !== -1
  127. ) {
  128. result.push(child);
  129. }
  130. }
  131. return result;
  132. }
  133. function queryNumericValue(node, tagName, namespace) {
  134. const resultNode = queryFirstNode(node, tagName, namespace);
  135. if (defined(resultNode)) {
  136. const result = parseFloat(resultNode.textContent);
  137. return !isNaN(result) ? result : undefined;
  138. }
  139. return undefined;
  140. }
  141. function queryStringValue(node, tagName, namespace) {
  142. const result = queryFirstNode(node, tagName, namespace);
  143. if (defined(result)) {
  144. return result.textContent.trim();
  145. }
  146. return undefined;
  147. }
  148. function createDefaultBillboard(image) {
  149. const billboard = new BillboardGraphics();
  150. billboard.width = BILLBOARD_SIZE;
  151. billboard.height = BILLBOARD_SIZE;
  152. billboard.scaleByDistance = new NearFarScalar(
  153. BILLBOARD_NEAR_DISTANCE,
  154. BILLBOARD_NEAR_RATIO,
  155. BILLBOARD_FAR_DISTANCE,
  156. BILLBOARD_FAR_RATIO
  157. );
  158. billboard.pixelOffsetScaleByDistance = new NearFarScalar(
  159. BILLBOARD_NEAR_DISTANCE,
  160. BILLBOARD_NEAR_RATIO,
  161. BILLBOARD_FAR_DISTANCE,
  162. BILLBOARD_FAR_RATIO
  163. );
  164. billboard.verticalOrigin = new ConstantProperty(VerticalOrigin.BOTTOM);
  165. billboard.image = image;
  166. return billboard;
  167. }
  168. function createDefaultLabel() {
  169. const label = new LabelGraphics();
  170. label.translucencyByDistance = new NearFarScalar(3000000, 1.0, 5000000, 0.0);
  171. label.pixelOffset = new Cartesian2(17, 0);
  172. label.horizontalOrigin = HorizontalOrigin.LEFT;
  173. label.font = "16px sans-serif";
  174. label.style = LabelStyle.FILL_AND_OUTLINE;
  175. return label;
  176. }
  177. function createDefaultPolyline(color) {
  178. const polyline = new PolylineGraphics();
  179. polyline.width = 4;
  180. polyline.material = new PolylineOutlineMaterialProperty();
  181. polyline.material.color = defined(color) ? color : Color.RED;
  182. polyline.material.outlineWidth = 2;
  183. polyline.material.outlineColor = Color.BLACK;
  184. return polyline;
  185. }
  186. // This is a list of the Optional Description Information:
  187. // <cmt> GPS comment of the waypoint
  188. // <desc> Descriptive description of the waypoint
  189. // <src> Source of the waypoint data
  190. // <type> Type (category) of waypoint
  191. const descriptiveInfoTypes = {
  192. time: {
  193. text: "Time",
  194. tag: "time",
  195. },
  196. comment: {
  197. text: "Comment",
  198. tag: "cmt",
  199. },
  200. description: {
  201. text: "Description",
  202. tag: "desc",
  203. },
  204. source: {
  205. text: "Source",
  206. tag: "src",
  207. },
  208. number: {
  209. text: "GPS track/route number",
  210. tag: "number",
  211. },
  212. type: {
  213. text: "Type",
  214. tag: "type",
  215. },
  216. };
  217. let scratchDiv;
  218. if (typeof document !== "undefined") {
  219. scratchDiv = document.createElement("div");
  220. }
  221. function processDescription(node, entity) {
  222. let i;
  223. let text = "";
  224. const infoTypeNames = Object.keys(descriptiveInfoTypes);
  225. const length = infoTypeNames.length;
  226. for (i = 0; i < length; i++) {
  227. const infoTypeName = infoTypeNames[i];
  228. const infoType = descriptiveInfoTypes[infoTypeName];
  229. infoType.value = defaultValue(
  230. queryStringValue(node, infoType.tag, namespaces.gpx),
  231. ""
  232. );
  233. if (defined(infoType.value) && infoType.value !== "") {
  234. text = `${text}<p>${infoType.text}: ${infoType.value}</p>`;
  235. }
  236. }
  237. if (!defined(text) || text === "") {
  238. // No description
  239. return;
  240. }
  241. // Turns non-explicit links into clickable links.
  242. text = autolinker.link(text);
  243. // Use a temporary div to manipulate the links
  244. // so that they open in a new window.
  245. scratchDiv.innerHTML = text;
  246. const links = scratchDiv.querySelectorAll("a");
  247. for (i = 0; i < links.length; i++) {
  248. links[i].setAttribute("target", "_blank");
  249. }
  250. const background = Color.WHITE;
  251. const foreground = Color.BLACK;
  252. let tmp = '<div class="cesium-infoBox-description-lighter" style="';
  253. tmp += "overflow:auto;";
  254. tmp += "word-wrap:break-word;";
  255. tmp += `background-color:${background.toCssColorString()};`;
  256. tmp += `color:${foreground.toCssColorString()};`;
  257. tmp += '">';
  258. tmp += `${scratchDiv.innerHTML}</div>`;
  259. scratchDiv.innerHTML = "";
  260. // return the final HTML as the description.
  261. return tmp;
  262. }
  263. function processWpt(dataSource, geometryNode, entityCollection, options) {
  264. const position = readCoordinateFromNode(geometryNode);
  265. const entity = getOrCreateEntity(geometryNode, entityCollection);
  266. entity.position = position;
  267. // Get billboard image
  268. const image = defined(options.waypointImage)
  269. ? options.waypointImage
  270. : dataSource._pinBuilder.fromMakiIconId(
  271. "marker",
  272. Color.RED,
  273. BILLBOARD_SIZE
  274. );
  275. entity.billboard = createDefaultBillboard(image);
  276. const name = queryStringValue(geometryNode, "name", namespaces.gpx);
  277. entity.name = name;
  278. entity.label = createDefaultLabel();
  279. entity.label.text = name;
  280. entity.description = processDescription(geometryNode, entity);
  281. if (options.clampToGround) {
  282. entity.billboard.heightReference = HeightReference.CLAMP_TO_GROUND;
  283. entity.label.heightReference = HeightReference.CLAMP_TO_GROUND;
  284. }
  285. }
  286. // rte represents route - an ordered list of waypoints representing a series of turn points leading to a destination
  287. function processRte(dataSource, geometryNode, entityCollection, options) {
  288. const entity = getOrCreateEntity(geometryNode, entityCollection);
  289. entity.description = processDescription(geometryNode, entity);
  290. // a list of waypoint
  291. const routePoints = queryNodes(geometryNode, "rtept", namespaces.gpx);
  292. const coordinateTuples = new Array(routePoints.length);
  293. for (let i = 0; i < routePoints.length; i++) {
  294. processWpt(dataSource, routePoints[i], entityCollection, options);
  295. coordinateTuples[i] = readCoordinateFromNode(routePoints[i]);
  296. }
  297. entity.polyline = createDefaultPolyline(options.routeColor);
  298. if (options.clampToGround) {
  299. entity.polyline.clampToGround = true;
  300. }
  301. entity.polyline.positions = coordinateTuples;
  302. }
  303. // trk represents a track - an ordered list of points describing a path.
  304. function processTrk(dataSource, geometryNode, entityCollection, options) {
  305. const entity = getOrCreateEntity(geometryNode, entityCollection);
  306. entity.description = processDescription(geometryNode, entity);
  307. const trackSegs = queryNodes(geometryNode, "trkseg", namespaces.gpx);
  308. let positions = [];
  309. let times = [];
  310. let trackSegInfo;
  311. let isTimeDynamic = true;
  312. const property = new SampledPositionProperty();
  313. for (let i = 0; i < trackSegs.length; i++) {
  314. trackSegInfo = processTrkSeg(trackSegs[i]);
  315. positions = positions.concat(trackSegInfo.positions);
  316. if (trackSegInfo.times.length > 0) {
  317. times = times.concat(trackSegInfo.times);
  318. property.addSamples(times, positions);
  319. // if one track segment is non dynamic the whole track must also be
  320. isTimeDynamic = isTimeDynamic && true;
  321. } else {
  322. isTimeDynamic = false;
  323. }
  324. }
  325. if (isTimeDynamic) {
  326. // Assign billboard image
  327. const image = defined(options.waypointImage)
  328. ? options.waypointImage
  329. : dataSource._pinBuilder.fromMakiIconId(
  330. "marker",
  331. Color.RED,
  332. BILLBOARD_SIZE
  333. );
  334. entity.billboard = createDefaultBillboard(image);
  335. entity.position = property;
  336. if (options.clampToGround) {
  337. entity.billboard.heightReference = HeightReference.CLAMP_TO_GROUND;
  338. }
  339. entity.availability = new TimeIntervalCollection();
  340. entity.availability.addInterval(
  341. new TimeInterval({
  342. start: times[0],
  343. stop: times[times.length - 1],
  344. })
  345. );
  346. }
  347. entity.polyline = createDefaultPolyline(options.trackColor);
  348. entity.polyline.positions = positions;
  349. if (options.clampToGround) {
  350. entity.polyline.clampToGround = true;
  351. }
  352. }
  353. function processTrkSeg(node) {
  354. const result = {
  355. positions: [],
  356. times: [],
  357. };
  358. const trackPoints = queryNodes(node, "trkpt", namespaces.gpx);
  359. let time;
  360. for (let i = 0; i < trackPoints.length; i++) {
  361. const position = readCoordinateFromNode(trackPoints[i]);
  362. result.positions.push(position);
  363. time = queryStringValue(trackPoints[i], "time", namespaces.gpx);
  364. if (defined(time)) {
  365. result.times.push(JulianDate.fromIso8601(time));
  366. }
  367. }
  368. return result;
  369. }
  370. // Processes a metadataType node and returns a metadata object
  371. // {@link http://www.topografix.com/gpx/1/1/#type_metadataType|GPX Schema}
  372. function processMetadata(node) {
  373. const metadataNode = queryFirstNode(node, "metadata", namespaces.gpx);
  374. if (defined(metadataNode)) {
  375. const metadata = {
  376. name: queryStringValue(metadataNode, "name", namespaces.gpx),
  377. desc: queryStringValue(metadataNode, "desc", namespaces.gpx),
  378. author: getPerson(metadataNode),
  379. copyright: getCopyright(metadataNode),
  380. link: getLink(metadataNode),
  381. time: queryStringValue(metadataNode, "time", namespaces.gpx),
  382. keywords: queryStringValue(metadataNode, "keywords", namespaces.gpx),
  383. bounds: getBounds(metadataNode),
  384. };
  385. if (
  386. defined(metadata.name) ||
  387. defined(metadata.desc) ||
  388. defined(metadata.author) ||
  389. defined(metadata.copyright) ||
  390. defined(metadata.link) ||
  391. defined(metadata.time) ||
  392. defined(metadata.keywords) ||
  393. defined(metadata.bounds)
  394. ) {
  395. return metadata;
  396. }
  397. }
  398. return undefined;
  399. }
  400. // Receives a XML node and returns a personType object, refer to
  401. // {@link http://www.topografix.com/gpx/1/1/#type_personType|GPX Schema}
  402. function getPerson(node) {
  403. const personNode = queryFirstNode(node, "author", namespaces.gpx);
  404. if (defined(personNode)) {
  405. const person = {
  406. name: queryStringValue(personNode, "name", namespaces.gpx),
  407. email: getEmail(personNode),
  408. link: getLink(personNode),
  409. };
  410. if (defined(person.name) || defined(person.email) || defined(person.link)) {
  411. return person;
  412. }
  413. }
  414. return undefined;
  415. }
  416. // Receives a XML node and returns an email address (from emailType), refer to
  417. // {@link http://www.topografix.com/gpx/1/1/#type_emailType|GPX Schema}
  418. function getEmail(node) {
  419. const emailNode = queryFirstNode(node, "email", namespaces.gpx);
  420. if (defined(emailNode)) {
  421. const id = queryStringValue(emailNode, "id", namespaces.gpx);
  422. const domain = queryStringValue(emailNode, "domain", namespaces.gpx);
  423. return `${id}@${domain}`;
  424. }
  425. return undefined;
  426. }
  427. // Receives a XML node and returns a linkType object, refer to
  428. // {@link http://www.topografix.com/gpx/1/1/#type_linkType|GPX Schema}
  429. function getLink(node) {
  430. const linkNode = queryFirstNode(node, "link", namespaces.gpx);
  431. if (defined(linkNode)) {
  432. const link = {
  433. href: queryStringAttribute(linkNode, "href"),
  434. text: queryStringValue(linkNode, "text", namespaces.gpx),
  435. mimeType: queryStringValue(linkNode, "type", namespaces.gpx),
  436. };
  437. if (defined(link.href) || defined(link.text) || defined(link.mimeType)) {
  438. return link;
  439. }
  440. }
  441. return undefined;
  442. }
  443. // Receives a XML node and returns a copyrightType object, refer to
  444. // {@link http://www.topografix.com/gpx/1/1/#type_copyrightType|GPX Schema}
  445. function getCopyright(node) {
  446. const copyrightNode = queryFirstNode(node, "copyright", namespaces.gpx);
  447. if (defined(copyrightNode)) {
  448. const copyright = {
  449. author: queryStringAttribute(copyrightNode, "author"),
  450. year: queryStringValue(copyrightNode, "year", namespaces.gpx),
  451. license: queryStringValue(copyrightNode, "license", namespaces.gpx),
  452. };
  453. if (
  454. defined(copyright.author) ||
  455. defined(copyright.year) ||
  456. defined(copyright.license)
  457. ) {
  458. return copyright;
  459. }
  460. }
  461. return undefined;
  462. }
  463. // Receives a XML node and returns a boundsType object, refer to
  464. // {@link http://www.topografix.com/gpx/1/1/#type_boundsType|GPX Schema}
  465. function getBounds(node) {
  466. const boundsNode = queryFirstNode(node, "bounds", namespaces.gpx);
  467. if (defined(boundsNode)) {
  468. const bounds = {
  469. minLat: queryNumericValue(boundsNode, "minlat", namespaces.gpx),
  470. maxLat: queryNumericValue(boundsNode, "maxlat", namespaces.gpx),
  471. minLon: queryNumericValue(boundsNode, "minlon", namespaces.gpx),
  472. maxLon: queryNumericValue(boundsNode, "maxlon", namespaces.gpx),
  473. };
  474. if (
  475. defined(bounds.minLat) ||
  476. defined(bounds.maxLat) ||
  477. defined(bounds.minLon) ||
  478. defined(bounds.maxLon)
  479. ) {
  480. return bounds;
  481. }
  482. }
  483. return undefined;
  484. }
  485. const complexTypes = {
  486. wpt: processWpt,
  487. rte: processRte,
  488. trk: processTrk,
  489. };
  490. function processGpx(dataSource, node, entityCollection, options) {
  491. const complexTypeNames = Object.keys(complexTypes);
  492. const complexTypeNamesLength = complexTypeNames.length;
  493. for (let i = 0; i < complexTypeNamesLength; i++) {
  494. const typeName = complexTypeNames[i];
  495. const processComplexTypeNode = complexTypes[typeName];
  496. const childNodes = node.childNodes;
  497. const length = childNodes.length;
  498. for (let q = 0; q < length; q++) {
  499. const child = childNodes[q];
  500. if (
  501. child.localName === typeName &&
  502. namespaces.gpx.indexOf(child.namespaceURI) !== -1
  503. ) {
  504. processComplexTypeNode(dataSource, child, entityCollection, options);
  505. }
  506. }
  507. }
  508. }
  509. function loadGpx(dataSource, gpx, options) {
  510. const entityCollection = dataSource._entityCollection;
  511. entityCollection.removeAll();
  512. const element = gpx.documentElement;
  513. const version = queryStringAttribute(element, "version");
  514. const creator = queryStringAttribute(element, "creator");
  515. let name;
  516. const metadata = processMetadata(element);
  517. if (defined(metadata)) {
  518. name = metadata.name;
  519. }
  520. if (element.localName === "gpx") {
  521. processGpx(dataSource, element, entityCollection, options);
  522. } else {
  523. console.log(`GPX - Unsupported node: ${element.localName}`);
  524. }
  525. let clock;
  526. const availability = entityCollection.computeAvailability();
  527. let start = availability.start;
  528. let stop = availability.stop;
  529. const isMinStart = JulianDate.equals(start, Iso8601.MINIMUM_VALUE);
  530. const isMaxStop = JulianDate.equals(stop, Iso8601.MAXIMUM_VALUE);
  531. if (!isMinStart || !isMaxStop) {
  532. let date;
  533. // If start is min time just start at midnight this morning, local time
  534. if (isMinStart) {
  535. date = new Date();
  536. date.setHours(0, 0, 0, 0);
  537. start = JulianDate.fromDate(date);
  538. }
  539. // If stop is max value just stop at midnight tonight, local time
  540. if (isMaxStop) {
  541. date = new Date();
  542. date.setHours(24, 0, 0, 0);
  543. stop = JulianDate.fromDate(date);
  544. }
  545. clock = new DataSourceClock();
  546. clock.startTime = start;
  547. clock.stopTime = stop;
  548. clock.currentTime = JulianDate.clone(start);
  549. clock.clockRange = ClockRange.LOOP_STOP;
  550. clock.clockStep = ClockStep.SYSTEM_CLOCK_MULTIPLIER;
  551. clock.multiplier = Math.round(
  552. Math.min(
  553. Math.max(JulianDate.secondsDifference(stop, start) / 60, 1),
  554. 3.15569e7
  555. )
  556. );
  557. }
  558. let changed = false;
  559. if (dataSource._name !== name) {
  560. dataSource._name = name;
  561. changed = true;
  562. }
  563. if (dataSource._creator !== creator) {
  564. dataSource._creator = creator;
  565. changed = true;
  566. }
  567. if (metadataChanged(dataSource._metadata, metadata)) {
  568. dataSource._metadata = metadata;
  569. changed = true;
  570. }
  571. if (dataSource._version !== version) {
  572. dataSource._version = version;
  573. changed = true;
  574. }
  575. if (clock !== dataSource._clock) {
  576. changed = true;
  577. dataSource._clock = clock;
  578. }
  579. if (changed) {
  580. dataSource._changed.raiseEvent(dataSource);
  581. }
  582. DataSource.setLoading(dataSource, false);
  583. return dataSource;
  584. }
  585. function metadataChanged(old, current) {
  586. if (!defined(old) && !defined(current)) {
  587. return false;
  588. } else if (defined(old) && defined(current)) {
  589. if (
  590. old.name !== current.name ||
  591. old.dec !== current.desc ||
  592. old.src !== current.src ||
  593. old.author !== current.author ||
  594. old.copyright !== current.copyright ||
  595. old.link !== current.link ||
  596. old.time !== current.time ||
  597. old.bounds !== current.bounds
  598. ) {
  599. return true;
  600. }
  601. return false;
  602. }
  603. return true;
  604. }
  605. function load(dataSource, entityCollection, data, options) {
  606. options = defaultValue(options, defaultValue.EMPTY_OBJECT);
  607. let promise = data;
  608. if (typeof data === "string" || data instanceof Resource) {
  609. data = Resource.createIfNeeded(data);
  610. promise = data.fetchBlob();
  611. // Add resource credits to our list of credits to display
  612. const resourceCredits = dataSource._resourceCredits;
  613. const credits = data.credits;
  614. if (defined(credits)) {
  615. const length = credits.length;
  616. for (let i = 0; i < length; i++) {
  617. resourceCredits.push(credits[i]);
  618. }
  619. }
  620. }
  621. return Promise.resolve(promise)
  622. .then(function (dataToLoad) {
  623. if (dataToLoad instanceof Blob) {
  624. return readBlobAsText(dataToLoad).then(function (text) {
  625. // There's no official way to validate if a parse was successful.
  626. // The following check detects the error on various browsers.
  627. // IE raises an exception
  628. let gpx;
  629. let error;
  630. try {
  631. gpx = parser.parseFromString(text, "application/xml");
  632. } catch (e) {
  633. error = e.toString();
  634. }
  635. // The parse succeeds on Chrome and Firefox, but the error
  636. // handling is different in each.
  637. if (
  638. defined(error) ||
  639. gpx.body ||
  640. gpx.documentElement.tagName === "parsererror"
  641. ) {
  642. // Firefox has error information as the firstChild nodeValue.
  643. let msg = defined(error)
  644. ? error
  645. : gpx.documentElement.firstChild.nodeValue;
  646. // Chrome has it in the body text.
  647. if (!msg) {
  648. msg = gpx.body.innerText;
  649. }
  650. // Return the error
  651. throw new RuntimeError(msg);
  652. }
  653. return loadGpx(dataSource, gpx, options);
  654. });
  655. }
  656. return loadGpx(dataSource, dataToLoad, options);
  657. })
  658. .catch(function (error) {
  659. dataSource._error.raiseEvent(dataSource, error);
  660. console.log(error);
  661. return Promise.reject(error);
  662. });
  663. }
  664. /**
  665. * A {@link DataSource} which processes the GPS Exchange Format (GPX).
  666. *
  667. * @alias GpxDataSource
  668. * @constructor
  669. *
  670. * @see {@link http://www.topografix.com/gpx.asp|Topografix GPX Standard}
  671. * @see {@link http://www.topografix.com/gpx/1/1/|Topografix GPX Documentation}
  672. *
  673. * @demo {@link http://sandcastle.cesium.com/index.html?src=GPX.html}
  674. *
  675. * @example
  676. * const viewer = new Cesium.Viewer('cesiumContainer');
  677. * viewer.dataSources.add(Cesium.GpxDataSource.load('../../SampleData/track.gpx'));
  678. */
  679. function GpxDataSource() {
  680. this._changed = new Event();
  681. this._error = new Event();
  682. this._loading = new Event();
  683. this._clock = undefined;
  684. this._entityCollection = new EntityCollection(this);
  685. this._entityCluster = new EntityCluster();
  686. this._name = undefined;
  687. this._version = undefined;
  688. this._creator = undefined;
  689. this._metadata = undefined;
  690. this._isLoading = false;
  691. this._pinBuilder = new PinBuilder();
  692. }
  693. /**
  694. * Creates a Promise to a new instance loaded with the provided GPX data.
  695. *
  696. * @param {string|Document|Blob} data A url, parsed GPX document, or Blob containing binary GPX data.
  697. * @param {object} [options] An object with the following properties:
  698. * @param {boolean} [options.clampToGround] True if the symbols should be rendered at the same height as the terrain
  699. * @param {string} [options.waypointImage] Image to use for waypoint billboards.
  700. * @param {string} [options.trackImage] Image to use for track billboards.
  701. * @param {string} [options.trackColor] Color to use for track lines.
  702. * @param {string} [options.routeColor] Color to use for route lines.
  703. * @returns {Promise<GpxDataSource>} A promise that will resolve to a new GpxDataSource instance once the gpx is loaded.
  704. */
  705. GpxDataSource.load = function (data, options) {
  706. return new GpxDataSource().load(data, options);
  707. };
  708. Object.defineProperties(GpxDataSource.prototype, {
  709. /**
  710. * Gets a human-readable name for this instance.
  711. * This will be automatically be set to the GPX document name on load.
  712. * @memberof GpxDataSource.prototype
  713. * @type {string}
  714. */
  715. name: {
  716. get: function () {
  717. return this._name;
  718. },
  719. },
  720. /**
  721. * Gets the version of the GPX Schema in use.
  722. * @memberof GpxDataSource.prototype
  723. * @type {string}
  724. */
  725. version: {
  726. get: function () {
  727. return this._version;
  728. },
  729. },
  730. /**
  731. * Gets the creator of the GPX document.
  732. * @memberof GpxDataSource.prototype
  733. * @type {string}
  734. */
  735. creator: {
  736. get: function () {
  737. return this._creator;
  738. },
  739. },
  740. /**
  741. * Gets an object containing metadata about the GPX file.
  742. * @memberof GpxDataSource.prototype
  743. * @type {object}
  744. */
  745. metadata: {
  746. get: function () {
  747. return this._metadata;
  748. },
  749. },
  750. /**
  751. * Gets the clock settings defined by the loaded GPX. This represents the total
  752. * availability interval for all time-dynamic data. If the GPX does not contain
  753. * time-dynamic data, this value is undefined.
  754. * @memberof GpxDataSource.prototype
  755. * @type {DataSourceClock}
  756. */
  757. clock: {
  758. get: function () {
  759. return this._clock;
  760. },
  761. },
  762. /**
  763. * Gets the collection of {@link Entity} instances.
  764. * @memberof GpxDataSource.prototype
  765. * @type {EntityCollection}
  766. */
  767. entities: {
  768. get: function () {
  769. return this._entityCollection;
  770. },
  771. },
  772. /**
  773. * Gets a value indicating if the data source is currently loading data.
  774. * @memberof GpxDataSource.prototype
  775. * @type {boolean}
  776. */
  777. isLoading: {
  778. get: function () {
  779. return this._isLoading;
  780. },
  781. },
  782. /**
  783. * Gets an event that will be raised when the underlying data changes.
  784. * @memberof GpxDataSource.prototype
  785. * @type {Event}
  786. */
  787. changedEvent: {
  788. get: function () {
  789. return this._changed;
  790. },
  791. },
  792. /**
  793. * Gets an event that will be raised if an error is encountered during processing.
  794. * @memberof GpxDataSource.prototype
  795. * @type {Event}
  796. */
  797. errorEvent: {
  798. get: function () {
  799. return this._error;
  800. },
  801. },
  802. /**
  803. * Gets an event that will be raised when the data source either starts or stops loading.
  804. * @memberof GpxDataSource.prototype
  805. * @type {Event}
  806. */
  807. loadingEvent: {
  808. get: function () {
  809. return this._loading;
  810. },
  811. },
  812. /**
  813. * Gets whether or not this data source should be displayed.
  814. * @memberof GpxDataSource.prototype
  815. * @type {boolean}
  816. */
  817. show: {
  818. get: function () {
  819. return this._entityCollection.show;
  820. },
  821. set: function (value) {
  822. this._entityCollection.show = value;
  823. },
  824. },
  825. /**
  826. * Gets or sets the clustering options for this data source. This object can be shared between multiple data sources.
  827. *
  828. * @memberof GpxDataSource.prototype
  829. * @type {EntityCluster}
  830. */
  831. clustering: {
  832. get: function () {
  833. return this._entityCluster;
  834. },
  835. set: function (value) {
  836. //>>includeStart('debug', pragmas.debug);
  837. if (!defined(value)) {
  838. throw new DeveloperError("value must be defined.");
  839. }
  840. //>>includeEnd('debug');
  841. this._entityCluster = value;
  842. },
  843. },
  844. });
  845. /**
  846. * Updates the data source to the provided time. This function is optional and
  847. * is not required to be implemented. It is provided for data sources which
  848. * retrieve data based on the current animation time or scene state.
  849. * If implemented, update will be called by {@link DataSourceDisplay} once a frame.
  850. *
  851. * @param {JulianDate} time The simulation time.
  852. * @returns {boolean} True if this data source is ready to be displayed at the provided time, false otherwise.
  853. */
  854. GpxDataSource.prototype.update = function (time) {
  855. return true;
  856. };
  857. /**
  858. * Asynchronously loads the provided GPX data, replacing any existing data.
  859. *
  860. * @param {string|Document|Blob} data A url, parsed GPX document, or Blob containing binary GPX data or a parsed GPX document.
  861. * @param {object} [options] An object with the following properties:
  862. * @param {boolean} [options.clampToGround] True if the symbols should be rendered at the same height as the terrain
  863. * @param {string} [options.waypointImage] Image to use for waypoint billboards.
  864. * @param {string} [options.trackImage] Image to use for track billboards.
  865. * @param {string} [options.trackColor] Color to use for track lines.
  866. * @param {string} [options.routeColor] Color to use for route lines.
  867. * @returns {Promise<GpxDataSource>} A promise that will resolve to this instances once the GPX is loaded.
  868. */
  869. GpxDataSource.prototype.load = function (data, options) {
  870. if (!defined(data)) {
  871. throw new DeveloperError("data is required.");
  872. }
  873. options = defaultValue(options, defaultValue.EMPTY_OBJECT);
  874. DataSource.setLoading(this, true);
  875. const oldName = this._name;
  876. const that = this;
  877. return load(this, this._entityCollection, data, options)
  878. .then(function () {
  879. let clock;
  880. const availability = that._entityCollection.computeAvailability();
  881. let start = availability.start;
  882. let stop = availability.stop;
  883. const isMinStart = JulianDate.equals(start, Iso8601.MINIMUM_VALUE);
  884. const isMaxStop = JulianDate.equals(stop, Iso8601.MAXIMUM_VALUE);
  885. if (!isMinStart || !isMaxStop) {
  886. let date;
  887. // If start is min time just start at midnight this morning, local time
  888. if (isMinStart) {
  889. date = new Date();
  890. date.setHours(0, 0, 0, 0);
  891. start = JulianDate.fromDate(date);
  892. }
  893. // If stop is max value just stop at midnight tonight, local time
  894. if (isMaxStop) {
  895. date = new Date();
  896. date.setHours(24, 0, 0, 0);
  897. stop = JulianDate.fromDate(date);
  898. }
  899. clock = new DataSourceClock();
  900. clock.startTime = start;
  901. clock.stopTime = stop;
  902. clock.currentTime = JulianDate.clone(start);
  903. clock.clockRange = ClockRange.LOOP_STOP;
  904. clock.clockStep = ClockStep.SYSTEM_CLOCK_MULTIPLIER;
  905. clock.multiplier = Math.round(
  906. Math.min(
  907. Math.max(JulianDate.secondsDifference(stop, start) / 60, 1),
  908. 3.15569e7
  909. )
  910. );
  911. }
  912. let changed = false;
  913. if (clock !== that._clock) {
  914. that._clock = clock;
  915. changed = true;
  916. }
  917. if (oldName !== that._name) {
  918. changed = true;
  919. }
  920. if (changed) {
  921. that._changed.raiseEvent(that);
  922. }
  923. DataSource.setLoading(that, false);
  924. return that;
  925. })
  926. .catch(function (error) {
  927. DataSource.setLoading(that, false);
  928. that._error.raiseEvent(that, error);
  929. console.log(error);
  930. return Promise.reject(error);
  931. });
  932. };
  933. export default GpxDataSource;