viewerDragDropMixin.js 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. import defaultValue from "../../Core/defaultValue.js";
  2. import defined from "../../Core/defined.js";
  3. import DeveloperError from "../../Core/DeveloperError.js";
  4. import Event from "../../Core/Event.js";
  5. import wrapFunction from "../../Core/wrapFunction.js";
  6. import CzmlDataSource from "../../DataSources/CzmlDataSource.js";
  7. import GeoJsonDataSource from "../../DataSources/GeoJsonDataSource.js";
  8. import KmlDataSource from "../../DataSources/KmlDataSource.js";
  9. import GpxDataSource from "../../DataSources/GpxDataSource.js";
  10. import getElement from "../getElement.js";
  11. /**
  12. * A mixin which adds default drag and drop support for CZML files to the Viewer widget.
  13. * Rather than being called directly, this function is normally passed as
  14. * a parameter to {@link Viewer#extend}, as shown in the example below.
  15. * @function viewerDragDropMixin
  16. * @param {Viewer} viewer The viewer instance.
  17. * @param {Object} [options] Object with the following properties:
  18. * @param {Element|String} [options.dropTarget=viewer.container] The DOM element which will serve as the drop target.
  19. * @param {Boolean} [options.clearOnDrop=true] When true, dropping files will clear all existing data sources first, when false, new data sources will be loaded after the existing ones.
  20. * @param {Boolean} [options.flyToOnDrop=true] When true, dropping files will fly to the data source once it is loaded.
  21. * @param {Boolean} [options.clampToGround=true] When true, datasources are clamped to the ground.
  22. * @param {Proxy} [options.proxy] The proxy to be used for KML network links.
  23. *
  24. * @exception {DeveloperError} Element with id <options.dropTarget> does not exist in the document.
  25. * @exception {DeveloperError} dropTarget is already defined by another mixin.
  26. * @exception {DeveloperError} dropEnabled is already defined by another mixin.
  27. * @exception {DeveloperError} dropError is already defined by another mixin.
  28. * @exception {DeveloperError} clearOnDrop is already defined by another mixin.
  29. *
  30. * @example
  31. * // Add basic drag and drop support and pop up an alert window on error.
  32. * const viewer = new Cesium.Viewer('cesiumContainer');
  33. * viewer.extend(Cesium.viewerDragDropMixin);
  34. * viewer.dropError.addEventListener(function(viewerArg, source, error) {
  35. * window.alert('Error processing ' + source + ':' + error);
  36. * });
  37. */
  38. function viewerDragDropMixin(viewer, options) {
  39. //>>includeStart('debug', pragmas.debug);
  40. if (!defined(viewer)) {
  41. throw new DeveloperError("viewer is required.");
  42. }
  43. if (viewer.hasOwnProperty("dropTarget")) {
  44. throw new DeveloperError("dropTarget is already defined by another mixin.");
  45. }
  46. if (viewer.hasOwnProperty("dropEnabled")) {
  47. throw new DeveloperError(
  48. "dropEnabled is already defined by another mixin."
  49. );
  50. }
  51. if (viewer.hasOwnProperty("dropError")) {
  52. throw new DeveloperError("dropError is already defined by another mixin.");
  53. }
  54. if (viewer.hasOwnProperty("clearOnDrop")) {
  55. throw new DeveloperError(
  56. "clearOnDrop is already defined by another mixin."
  57. );
  58. }
  59. if (viewer.hasOwnProperty("flyToOnDrop")) {
  60. throw new DeveloperError(
  61. "flyToOnDrop is already defined by another mixin."
  62. );
  63. }
  64. //>>includeEnd('debug');
  65. options = defaultValue(options, defaultValue.EMPTY_OBJECT);
  66. //Local variables to be closed over by defineProperties.
  67. let dropEnabled = true;
  68. let flyToOnDrop = defaultValue(options.flyToOnDrop, true);
  69. const dropError = new Event();
  70. let clearOnDrop = defaultValue(options.clearOnDrop, true);
  71. let dropTarget = defaultValue(options.dropTarget, viewer.container);
  72. let clampToGround = defaultValue(options.clampToGround, true);
  73. let proxy = options.proxy;
  74. dropTarget = getElement(dropTarget);
  75. Object.defineProperties(viewer, {
  76. /**
  77. * Gets or sets the element to serve as the drop target.
  78. * @memberof viewerDragDropMixin.prototype
  79. * @type {Element}
  80. */
  81. dropTarget: {
  82. //TODO See https://github.com/CesiumGS/cesium/issues/832
  83. get: function () {
  84. return dropTarget;
  85. },
  86. set: function (value) {
  87. //>>includeStart('debug', pragmas.debug);
  88. if (!defined(value)) {
  89. throw new DeveloperError("value is required.");
  90. }
  91. //>>includeEnd('debug');
  92. unsubscribe(dropTarget, handleDrop);
  93. dropTarget = value;
  94. subscribe(dropTarget, handleDrop);
  95. },
  96. },
  97. /**
  98. * Gets or sets a value indicating if drag and drop support is enabled.
  99. * @memberof viewerDragDropMixin.prototype
  100. * @type {Element}
  101. */
  102. dropEnabled: {
  103. get: function () {
  104. return dropEnabled;
  105. },
  106. set: function (value) {
  107. if (value !== dropEnabled) {
  108. if (value) {
  109. subscribe(dropTarget, handleDrop);
  110. } else {
  111. unsubscribe(dropTarget, handleDrop);
  112. }
  113. dropEnabled = value;
  114. }
  115. },
  116. },
  117. /**
  118. * Gets the event that will be raised when an error is encountered during drop processing.
  119. * @memberof viewerDragDropMixin.prototype
  120. * @type {Event}
  121. */
  122. dropError: {
  123. get: function () {
  124. return dropError;
  125. },
  126. },
  127. /**
  128. * Gets or sets a value indicating if existing data sources should be cleared before adding the newly dropped sources.
  129. * @memberof viewerDragDropMixin.prototype
  130. * @type {Boolean}
  131. */
  132. clearOnDrop: {
  133. get: function () {
  134. return clearOnDrop;
  135. },
  136. set: function (value) {
  137. clearOnDrop = value;
  138. },
  139. },
  140. /**
  141. * Gets or sets a value indicating if the camera should fly to the data source after it is loaded.
  142. * @memberof viewerDragDropMixin.prototype
  143. * @type {Boolean}
  144. */
  145. flyToOnDrop: {
  146. get: function () {
  147. return flyToOnDrop;
  148. },
  149. set: function (value) {
  150. flyToOnDrop = value;
  151. },
  152. },
  153. /**
  154. * Gets or sets the proxy to be used for KML.
  155. * @memberof viewerDragDropMixin.prototype
  156. * @type {Proxy}
  157. */
  158. proxy: {
  159. get: function () {
  160. return proxy;
  161. },
  162. set: function (value) {
  163. proxy = value;
  164. },
  165. },
  166. /**
  167. * Gets or sets a value indicating if the datasources should be clamped to the ground
  168. * @memberof viewerDragDropMixin.prototype
  169. * @type {Boolean}
  170. */
  171. clampToGround: {
  172. get: function () {
  173. return clampToGround;
  174. },
  175. set: function (value) {
  176. clampToGround = value;
  177. },
  178. },
  179. });
  180. function handleDrop(event) {
  181. stop(event);
  182. if (clearOnDrop) {
  183. viewer.entities.removeAll();
  184. viewer.dataSources.removeAll();
  185. }
  186. const files = event.dataTransfer.files;
  187. const length = files.length;
  188. for (let i = 0; i < length; i++) {
  189. const file = files[i];
  190. const reader = new FileReader();
  191. reader.onload = createOnLoadCallback(viewer, file, proxy, clampToGround);
  192. reader.onerror = createDropErrorCallback(viewer, file);
  193. reader.readAsText(file);
  194. }
  195. }
  196. //Enable drop by default;
  197. subscribe(dropTarget, handleDrop);
  198. //Wrap the destroy function to make sure all events are unsubscribed from
  199. viewer.destroy = wrapFunction(viewer, viewer.destroy, function () {
  200. viewer.dropEnabled = false;
  201. });
  202. //Specs need access to handleDrop
  203. viewer._handleDrop = handleDrop;
  204. }
  205. function stop(event) {
  206. event.stopPropagation();
  207. event.preventDefault();
  208. }
  209. function unsubscribe(dropTarget, handleDrop) {
  210. const currentTarget = dropTarget;
  211. if (defined(currentTarget)) {
  212. currentTarget.removeEventListener("drop", handleDrop, false);
  213. currentTarget.removeEventListener("dragenter", stop, false);
  214. currentTarget.removeEventListener("dragover", stop, false);
  215. currentTarget.removeEventListener("dragexit", stop, false);
  216. }
  217. }
  218. function subscribe(dropTarget, handleDrop) {
  219. dropTarget.addEventListener("drop", handleDrop, false);
  220. dropTarget.addEventListener("dragenter", stop, false);
  221. dropTarget.addEventListener("dragover", stop, false);
  222. dropTarget.addEventListener("dragexit", stop, false);
  223. }
  224. function createOnLoadCallback(viewer, file, proxy, clampToGround) {
  225. const scene = viewer.scene;
  226. return function (evt) {
  227. const fileName = file.name;
  228. try {
  229. let loadPromise;
  230. if (/\.czml$/i.test(fileName)) {
  231. loadPromise = CzmlDataSource.load(JSON.parse(evt.target.result), {
  232. sourceUri: fileName,
  233. });
  234. } else if (
  235. /\.geojson$/i.test(fileName) ||
  236. /\.json$/i.test(fileName) ||
  237. /\.topojson$/i.test(fileName)
  238. ) {
  239. loadPromise = GeoJsonDataSource.load(JSON.parse(evt.target.result), {
  240. sourceUri: fileName,
  241. clampToGround: clampToGround,
  242. });
  243. } else if (/\.(kml|kmz)$/i.test(fileName)) {
  244. loadPromise = KmlDataSource.load(file, {
  245. sourceUri: fileName,
  246. proxy: proxy,
  247. camera: scene.camera,
  248. canvas: scene.canvas,
  249. clampToGround: clampToGround,
  250. screenOverlayContainer: viewer.container,
  251. });
  252. } else if (/\.gpx$/i.test(fileName)) {
  253. loadPromise = GpxDataSource.load(file, {
  254. sourceUri: fileName,
  255. proxy: proxy,
  256. });
  257. } else {
  258. viewer.dropError.raiseEvent(
  259. viewer,
  260. fileName,
  261. `Unrecognized file: ${fileName}`
  262. );
  263. return;
  264. }
  265. if (defined(loadPromise)) {
  266. viewer.dataSources
  267. .add(loadPromise)
  268. .then(function (dataSource) {
  269. if (viewer.flyToOnDrop) {
  270. viewer.flyTo(dataSource);
  271. }
  272. })
  273. .catch(function (error) {
  274. viewer.dropError.raiseEvent(viewer, fileName, error);
  275. });
  276. }
  277. } catch (error) {
  278. viewer.dropError.raiseEvent(viewer, fileName, error);
  279. }
  280. };
  281. }
  282. function createDropErrorCallback(viewer, file) {
  283. return function (evt) {
  284. viewer.dropError.raiseEvent(viewer, file.name, evt.target.error);
  285. };
  286. }
  287. export default viewerDragDropMixin;