Source: map-common/src/main/resources/META-INF/resources/js/MapBase.es.js

  1. /**
  2. * Copyright (c) 2000-present Liferay, Inc. All rights reserved.
  3. *
  4. * This library is free software; you can redistribute it and/or modify it under
  5. * the terms of the GNU Lesser General Public License as published by the Free
  6. * Software Foundation; either version 2.1 of the License, or (at your option)
  7. * any later version.
  8. *
  9. * This library is distributed in the hope that it will be useful, but WITHOUT
  10. * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  11. * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
  12. * details.
  13. */
  14. import {buildFragment} from 'metal-dom';
  15. import State, {Config} from 'metal-state';
  16. import GeoJSONBase from './GeoJSONBase.es';
  17. import MarkerBase from './MarkerBase.es';
  18. import {isSubsetOf} from './validators.es';
  19. /**
  20. * HTML template string used for generating the home button that is
  21. * used for centering the map at the user location or the original position.
  22. * @review
  23. * @type {string}
  24. */
  25. const TPL_HOME_BUTTON = `
  26. <button class='btn btn-secondary home-button'>
  27. <i class='glyphicon glyphicon-screenshot'></i>
  28. </button>
  29. `;
  30. /**
  31. * HTML template string used for generating the search box that is used
  32. * for looking for map locations.
  33. * @review
  34. * @type {string}
  35. */
  36. const TPL_SEARCH_BOX = `
  37. <div class='col-md-6 search-controls'>
  38. <input class='search-input' placeholder='' type='text' />
  39. <div>
  40. `;
  41. /**
  42. * Object that will hold callbacks waiting for being executed
  43. * when maps are created.
  44. * @review
  45. * @see MapBase.register()
  46. * @see MapBase.get()
  47. */
  48. const pendingCallbacks = {};
  49. /**
  50. * MapBase
  51. * Each instance represents the map object itself, and adds
  52. * all necesary listeners and object specified in the given
  53. * configuration. This class is the core of the module, and
  54. * will create instances of all the other objects, including
  55. * the native maps implemented by inheriting classes.
  56. * @abstract
  57. * @review
  58. */
  59. class MapBase extends State {
  60. /**
  61. * MapBase constructor
  62. * @param {Array} args List of arguments to be sent to State constructor
  63. * @review
  64. */
  65. constructor(...args) {
  66. super(...args);
  67. this._customControls = {};
  68. this._dialog = null;
  69. this._eventHandlers = [];
  70. this._geoJSONLayer = null;
  71. this._geocoder = null;
  72. this._geolocationMarker = null;
  73. this._map = null;
  74. this._originalPosition = null;
  75. this._handleGeoJSONLayerFeatureClicked = this._handleGeoJSONLayerFeatureClicked.bind(
  76. this
  77. );
  78. this._handleGeoJSONLayerFeaturesAdded = this._handleGeoJSONLayerFeaturesAdded.bind(
  79. this
  80. );
  81. this._handleGeoLocationMarkerDragended = this._handleGeoLocationMarkerDragended.bind(
  82. this
  83. );
  84. this._handleHomeButtonClicked = this._handleHomeButtonClicked.bind(
  85. this
  86. );
  87. this._handlePositionChanged = this._handlePositionChanged.bind(this);
  88. this._handleSearchButtonClicked = this._handleSearchButtonClicked.bind(
  89. this
  90. );
  91. this.on('positionChange', this._handlePositionChanged);
  92. const geolocation =
  93. this.position && this.position.location
  94. ? this.position.location
  95. : {};
  96. if (!geolocation.lat || !geolocation.lng) {
  97. Liferay.Util.getGeolocation(
  98. (lat, lng) => {
  99. this._initializeLocation({lat, lng});
  100. },
  101. () => {
  102. this.zoom = 2;
  103. this._initializeLocation({lat: 0, lng: 0});
  104. }
  105. );
  106. }
  107. else {
  108. this._initializeLocation(geolocation);
  109. }
  110. }
  111. /**
  112. * Destroys the existing _geoJSONLayer and _customControls[SEARCH]
  113. * @review
  114. * @see MapBase._initializeMap()
  115. * @see MapBase._createCustomControls()
  116. */
  117. destructor() {
  118. if (this._geoJSONLayer) {
  119. this._geoJSONLayer.dispose();
  120. this._geoJSONLayer = null;
  121. }
  122. if (
  123. this._customControls &&
  124. this._customControls[this.constructor.CONTROLS.SEARCH]
  125. ) {
  126. this._customControls[this.constructor.CONTROLS.SEARCH].dispose();
  127. this._customControls[this.constructor.CONTROLS.SEARCH] = null;
  128. }
  129. }
  130. /**
  131. * @protected
  132. * @review
  133. *
  134. * Add event listeners to:
  135. * @see this._geoJSONLayer
  136. * @see this._geolocationMarker
  137. * @see this._customControls
  138. *
  139. * All added listeners are implemented as binded methods:
  140. * @see this._handleGeoJSONLayerFeaturesAdded
  141. * @see this._handleGeoJSONLayerFeatureClicked
  142. * @see this._handleGeoLocationMarkerDragended
  143. * @see this._handleHomeButtonClicked
  144. * @see this._handleSearchButtonClicked
  145. */
  146. _bindUIMB() {
  147. if (this._geoJSONLayer) {
  148. this._geoJSONLayer.on(
  149. 'featuresAdded',
  150. this._handleGeoJSONLayerFeaturesAdded
  151. );
  152. this._geoJSONLayer.on(
  153. 'featureClick',
  154. this._handleGeoJSONLayerFeatureClicked
  155. );
  156. }
  157. if (this._geolocationMarker) {
  158. this._geolocationMarker.on(
  159. 'dragend',
  160. this._handleGeoLocationMarkerDragended
  161. );
  162. }
  163. if (this._customControls) {
  164. const homeControl = this._customControls[
  165. this.constructor.CONTROLS.HOME
  166. ];
  167. const searchControl = this._customControls[
  168. this.constructor.CONTROLS.SEARCH
  169. ];
  170. if (homeControl) {
  171. homeControl.addEventListener(
  172. 'click',
  173. this._handleHomeButtonClicked
  174. );
  175. }
  176. if (searchControl) {
  177. searchControl.on('search', this._handleSearchButtonClicked);
  178. }
  179. }
  180. }
  181. /**
  182. * Creates existing custom controls and stores them inside _customControls.
  183. * It only adds the home button and the search box if the corresponding
  184. * flags are activated inside MapBase.controls.
  185. * @protected
  186. * @review
  187. */
  188. _createCustomControls() {
  189. const controls = this.controls || [];
  190. const customControls = {};
  191. if (controls.indexOf(this.constructor.CONTROLS.HOME) !== -1) {
  192. const homeControl = buildFragment(TPL_HOME_BUTTON).querySelector(
  193. '.btn.btn-secondary.home-button'
  194. );
  195. customControls[this.constructor.CONTROLS.HOME] = homeControl;
  196. this.addControl(
  197. homeControl,
  198. this.constructor.POSITION.RIGHT_BOTTOM
  199. );
  200. }
  201. if (
  202. controls.indexOf(this.constructor.CONTROLS.SEARCH) !== -1 &&
  203. this.constructor.SearchImpl
  204. ) {
  205. const searchControl = buildFragment(TPL_SEARCH_BOX).querySelector(
  206. 'div.col-md-6.search-controls'
  207. );
  208. customControls[
  209. this.constructor.CONTROLS.SEARCH
  210. ] = new this.constructor.SearchImpl({
  211. inputNode: searchControl.querySelector('input'),
  212. });
  213. this.addControl(searchControl, this.constructor.POSITION.TOP_LEFT);
  214. }
  215. this._customControls = customControls;
  216. }
  217. /**
  218. * Creates a new map for the given location and controlsConfig.
  219. * @abstract
  220. * @param {Object} location
  221. * @param {Object} controlsConfig
  222. * @protected
  223. * @return {Object} Created map
  224. * @review
  225. */
  226. _createMap(
  227. /* eslint-disable no-unused-vars */
  228. location,
  229. controlsConfig
  230. /* eslint-enable no-unused-vars */
  231. ) {
  232. throw new Error('This method must be implemented');
  233. }
  234. /**
  235. * Returns an object with the control configuration associated to the
  236. * existing controls (MapBase.controls).
  237. * @protected
  238. * @return {Object} Object with the control options with the following shape:
  239. * for each control, there is a corresponding [control] attribute with a
  240. * boolean value indicating if there is a configuration for the control. If
  241. * true, where will be a [control]Options attribute with the configuration
  242. * content.
  243. * @review
  244. */
  245. _getControlsConfig() {
  246. const config = {};
  247. const availableControls = this.controls.map(item => {
  248. return typeof item === 'string' ? item : item.name;
  249. });
  250. Object.keys(this.constructor.CONTROLS_MAP).forEach(key => {
  251. const controlIndex = availableControls.indexOf(key);
  252. const value = this.constructor.CONTROLS_MAP[key];
  253. if (controlIndex > -1) {
  254. const controlConfig = this.controls[controlIndex];
  255. if (
  256. controlConfig &&
  257. typeof controlConfig === 'object' &&
  258. controlConfig.cfg
  259. ) {
  260. config[`${value}Options`] = controlConfig.cfg;
  261. }
  262. config[value] = controlIndex !== -1;
  263. }
  264. });
  265. return config;
  266. }
  267. /**
  268. * If there is an existing this._dialog, it returns it.
  269. * Otherwise, if the MapBase.DialogImpl class has been set, it creates
  270. * a new instance with the existing map and returns it.
  271. * @protected
  272. * @return {MapBase.DialogImpl|null}
  273. * @review
  274. */
  275. _getDialog() {
  276. if (!this._dialog && this.constructor.DialogImpl) {
  277. this._dialog = new this.constructor.DialogImpl({
  278. map: this._map,
  279. });
  280. }
  281. return this._dialog;
  282. }
  283. /**
  284. * If there is an existing this._geocoder, it returns it.
  285. * Otherwise, if the MapBase.GeocoderImpl class has been set, it creates
  286. * a new instance and returns it.
  287. * @protected
  288. * @return {MapBase.GeocoderImpl|null}
  289. * @review
  290. */
  291. _getGeocoder() {
  292. if (!this._geocoder && this.constructor.GeocoderImpl) {
  293. this._geocoder = new this.constructor.GeocoderImpl();
  294. }
  295. return this._geocoder;
  296. }
  297. /**
  298. * Event handler executed when any feature is added to the existing.
  299. * GeoJSONLayer. It will update the current position if necesary.
  300. * @param {{ features: Array<Object> }} param0 Array of features that
  301. * will be processed.
  302. * @protected
  303. * @review
  304. * @see MapBase.getBounds()
  305. * @see MapBase.position
  306. */
  307. _handleGeoJSONLayerFeaturesAdded({features}) {
  308. const bounds = this.getBounds();
  309. const locations = features.map(feature => feature.getGeometry().get());
  310. if (locations.length > 1) {
  311. locations.forEach(location => bounds.extend(location));
  312. }
  313. else {
  314. this.position = {location: locations[0]};
  315. }
  316. }
  317. /**
  318. * Event handler executed when a GeoJSONLayer feature is clicked. It
  319. * simple propagates the event with the given feature.
  320. * @param {{ feature: Object }} param0 Feature to be propagated.
  321. * @protected
  322. * @review
  323. */
  324. _handleGeoJSONLayerFeatureClicked({feature}) {
  325. this.emit('featureClick', {feature});
  326. }
  327. /**
  328. * Event handler executed when the geolocation marker has been dragged to
  329. * a new position. It updates the instance position.
  330. * @param {{ location: Object }} param0 New marker location.
  331. * @protected
  332. * @review
  333. * @see MapBase._getGeoCoder()
  334. * @see MapBase.position
  335. */
  336. _handleGeoLocationMarkerDragended({location}) {
  337. this._getGeocoder().reverse(location, ({data}) => {
  338. this.position = data;
  339. });
  340. }
  341. /**
  342. * Event handler executed when the home button GeoJSONLayer feature is
  343. * clicked. It resets the instance position to _originalPosition and stops
  344. * the event propagation.
  345. * @param {Event} event Click event.
  346. * @review
  347. * @see MapBase.position
  348. * @see MapBase._originalPosition
  349. */
  350. _handleHomeButtonClicked(event) {
  351. event.preventDefault();
  352. this.position = this._originalPosition;
  353. }
  354. /**
  355. * Event handler executed when the user position changes. It centers the map
  356. * and, if existing, updates the _geolocationMarker position.
  357. * @param {{ newVal: { location: Object } }} param0 New location information.
  358. * @review
  359. * @see MapBase.setCenter()
  360. * @see MapBase._geolocationMarker
  361. */
  362. _handlePositionChanged({newVal: {location}}) {
  363. this.setCenter(location);
  364. if (this._geolocationMarker) {
  365. this._geolocationMarker.setPosition(location);
  366. }
  367. }
  368. /**
  369. * Event handler executed when the search button GeoJSONLayer feature has
  370. * been clicked. It updates the instance position.
  371. * @param {{ position: Object }} param0 New position.
  372. * @review
  373. * @see MapBase.position
  374. */
  375. _handleSearchButtonClicked({position}) {
  376. this.position = position;
  377. }
  378. /**
  379. * If this.data has a truthy value and this._geoJSONLayer has been
  380. * set, it tries to parse geoJSON information with _geoJSONLayer.addData()
  381. * @protected
  382. * @review
  383. * @see this._geoJSONLayer
  384. * @see this.data
  385. */
  386. _initializeGeoJSONData() {
  387. if (this.data && this._geoJSONLayer) {
  388. this._geoJSONLayer.addData(this.data);
  389. }
  390. }
  391. /**
  392. * Initializes the instance map using the given location and, if
  393. * this.geolocation property is true, tries to get the location using
  394. * the geocoder.
  395. * @param {Object} location Location object user for map initialization.
  396. * @protected
  397. * @review
  398. * @see this._initializeMap()
  399. * @see this._getGeocoder()
  400. */
  401. _initializeLocation(geolocation) {
  402. const geocoder = this._getGeocoder();
  403. if (this.geolocation && geocoder) {
  404. geocoder.reverse(geolocation, ({data}) =>
  405. this._initializeMap(data)
  406. );
  407. }
  408. else {
  409. this._initializeMap({location: geolocation});
  410. }
  411. }
  412. /**
  413. * Creates a new map with the given position and initialize the map
  414. * controls, loads the geoJSONData and, if geolocation is active, creates
  415. * a marker for it.
  416. * @param {Object} position Initial position added to the map.
  417. * @protected
  418. * @review
  419. * @see MapBase._getControlsConfig()
  420. * @see MapBase._createMap()
  421. * @see MapBase.GeoJSONImpl
  422. * @see MapBase.addMarker()
  423. * @see MapBase._createCustomControls()
  424. * @see MapBase._bindUIMB()
  425. * @see MapBase._initializeGeoJSONData()
  426. */
  427. _initializeMap(position) {
  428. const controlsConfig = this._getControlsConfig();
  429. const geolocation = position.location;
  430. this._originalPosition = position;
  431. this._map = this._createMap(geolocation, controlsConfig);
  432. if (
  433. this.constructor.GeoJSONImpl &&
  434. this.constructor.GeoJSONImpl !== GeoJSONBase
  435. ) {
  436. this._geoJSONLayer = new this.constructor.GeoJSONImpl({
  437. map: this._map,
  438. });
  439. }
  440. if (this.geolocation) {
  441. this._geolocationMarker = this.addMarker(geolocation);
  442. }
  443. this.position = position;
  444. this._createCustomControls();
  445. this._bindUIMB();
  446. this._initializeGeoJSONData();
  447. }
  448. /**
  449. * Adds a new control to the interface at the given position.
  450. * @param {Object} control Native control object
  451. * @param {MapBase.POSITION} position Position defined in MapBase class
  452. * @review
  453. */
  454. addControl(
  455. /* eslint-disable no-unused-vars */
  456. control,
  457. position
  458. /* eslint-enable no-unused-vars */
  459. ) {
  460. throw new Error('This method must be implemented');
  461. }
  462. /**
  463. * Returns the map bounds.
  464. * @abstract
  465. * @return {Object} Map bounds
  466. * @review
  467. */
  468. getBounds() {
  469. throw new Error('This method must be implemented');
  470. }
  471. /**
  472. * Centers the map on the given location.
  473. * @abstract
  474. * @param {Object} location
  475. * @review
  476. */
  477. setCenter(
  478. /* eslint-disable no-unused-vars */
  479. location
  480. /* eslint-enable no-unused-vars */
  481. ) {
  482. throw new Error('This method must be implemented');
  483. }
  484. /**
  485. * If the class MapBase.MarkerImpl has been specified, creates an instance
  486. * of this class with the given location and the existing this._map object
  487. * and returns it.
  488. * @param {Object} location Location object used for the marker position
  489. * @return {MapBase.MarkerImpl}
  490. * @review
  491. */
  492. addMarker(location) {
  493. let marker;
  494. if (
  495. this.constructor.MarkerImpl &&
  496. this.constructor.MarkerImpl !== MarkerBase
  497. ) {
  498. marker = new this.constructor.MarkerImpl({
  499. location,
  500. map: this._map,
  501. });
  502. }
  503. return marker;
  504. }
  505. /**
  506. * Returns the stored map
  507. * @return {Object}
  508. * @review
  509. * @see MapBase._initializeMap()
  510. */
  511. getNativeMap() {
  512. return this._map;
  513. }
  514. /**
  515. * Adds a listener for the given event using the given context. This
  516. * methods uses MetalJS' functionality, but overrides the context binding
  517. * in order to avoid breaking changes with the old implementation.
  518. * @param {string} eventName Event name that will be listened.
  519. * @param {function} callback Callback executed when the event fires.
  520. * @param {Object} [context] Optional context that will be used for binding
  521. * the callback when specified.
  522. * @return {*} Result of the State.on method.
  523. * @review
  524. * @see State.on
  525. */
  526. on(eventName, callback, context) {
  527. let boundCallback = callback;
  528. if (context) {
  529. boundCallback = callback.bind(context);
  530. }
  531. return super.on(eventName, boundCallback);
  532. }
  533. /**
  534. * Opens a dialog if this_getDialog() returns a valid object.
  535. * @param {*} dialogConfig Dialog configuration that will be sent to the
  536. * dialog.open() method.
  537. * @review
  538. * @see MapBase._getDialog()
  539. */
  540. openDialog(dialogConfig) {
  541. const dialog = this._getDialog();
  542. if (dialog) {
  543. dialog.open(dialogConfig);
  544. }
  545. }
  546. /**
  547. * Setter called everytime the position attribute is changed.
  548. * @param {Object} position New position
  549. * @return {Object} The given position object
  550. * @review
  551. */
  552. setPosition(position) {
  553. this.emit('positionChange', {
  554. newVal: {
  555. address: position.address,
  556. location: position.location,
  557. },
  558. });
  559. return position;
  560. }
  561. }
  562. /**
  563. * Registers the given callback to be executed when the map identified
  564. * by the given id has been created. If the map has already been created, it
  565. * is executed immediatly.
  566. * @param {string} id Id of the map that needs to be created
  567. * @param {function} callback Callback being executed
  568. * @review
  569. */
  570. MapBase.get = function(id, callback) {
  571. const map = Liferay.component(id);
  572. if (map) {
  573. callback(map);
  574. }
  575. else {
  576. const idPendingCallbacks = pendingCallbacks[id] || [];
  577. idPendingCallbacks.push(callback);
  578. pendingCallbacks[id] = idPendingCallbacks;
  579. }
  580. };
  581. /**
  582. * Registers the given map with the given id, and executes all existing
  583. * callbacks associated with the id. Then it clears the list of callbacks.
  584. * @param {string} id Id of the map that has been created
  585. * @param {Object} map Map that has been created
  586. * @param {string} portletId Id of the portlet that registers the map
  587. * @review
  588. */
  589. MapBase.register = function(id, map, portletId) {
  590. const componentConfig = portletId ? {portletId} : {destroyOnNavigate: true};
  591. Liferay.component(id, map, componentConfig);
  592. const idPendingCallbacks = pendingCallbacks[id];
  593. if (idPendingCallbacks) {
  594. idPendingCallbacks.forEach(callback => callback(map));
  595. idPendingCallbacks.length = 0;
  596. }
  597. };
  598. /**
  599. * Class that will be used for creating dialog objects.
  600. * @review
  601. */
  602. MapBase.DialogImpl = null;
  603. /**
  604. * Class that will be used for parsing geoposition information.
  605. * @review
  606. */
  607. MapBase.GeocoderImpl = null;
  608. /**
  609. * Class that will be used for creating GeoJSON instances.
  610. * This class must be replaced by another one extending GeoJSONBase.
  611. * @review
  612. */
  613. MapBase.GeoJSONImpl = GeoJSONBase;
  614. /**
  615. * Class that will be used for creating map markers.
  616. * This class must be replaced by another one extending MarkerBase.
  617. * @review
  618. */
  619. MapBase.MarkerImpl = MarkerBase;
  620. /**
  621. * Class that will be used for creating an instance of searchbox.
  622. * This class must be replaced by another one extending MarkerBase.
  623. * @review
  624. */
  625. MapBase.SearchImpl = null;
  626. /**
  627. * List of controls that maybe shown inside the rendered map.
  628. * @review
  629. */
  630. MapBase.CONTROLS = {
  631. ATTRIBUTION: 'attribution',
  632. GEOLOCATION: 'geolocation',
  633. HOME: 'home',
  634. OVERVIEW: 'overview',
  635. PAN: 'pan',
  636. ROTATE: 'rotate',
  637. SCALE: 'scale',
  638. SEARCH: 'search',
  639. STREETVIEW: 'streetview',
  640. TYPE: 'type',
  641. ZOOM: 'zoom',
  642. };
  643. /**
  644. * Control mapping that should be overriden by child classes.
  645. * @review
  646. */
  647. MapBase.CONTROLS_MAP = {};
  648. /**
  649. * Available map positions.
  650. * @review
  651. */
  652. MapBase.POSITION = {
  653. BOTTOM: 11,
  654. BOTTOM_CENTER: 11,
  655. BOTTOM_LEFT: 10,
  656. BOTTOM_RIGHT: 12,
  657. CENTER: 13,
  658. LEFT: 5,
  659. LEFT_BOTTOM: 6,
  660. LEFT_CENTER: 4,
  661. LEFT_TOP: 5,
  662. RIGHT: 7,
  663. RIGHT_BOTTOM: 9,
  664. RIGHT_CENTER: 8,
  665. RIGHT_TOP: 7,
  666. TOP: 2,
  667. TOP_CENTER: 2,
  668. TOP_LEFT: 1,
  669. TOP_RIGHT: 3,
  670. };
  671. /**
  672. * Position mapping that should be overriden by child classes.
  673. * @review
  674. */
  675. MapBase.POSITION_MAP = {};
  676. /**
  677. * State definition.
  678. * @review
  679. * @static
  680. * @type {!Object}
  681. */
  682. MapBase.STATE = {
  683. /**
  684. * DOM node selector identifying the element that will be used
  685. * for rendering the map
  686. * @review
  687. * @type {string}
  688. */
  689. boundingBox: Config.string().value(''),
  690. /**
  691. * List of controls that will be shown on the map.
  692. * The full control list is kept inside MapBase.CONTROLS.
  693. * @review
  694. * @type {Array<string>}
  695. */
  696. controls: Config.validator(
  697. isSubsetOf(Object.values(MapBase.CONTROLS))
  698. ).value([
  699. MapBase.CONTROLS.PAN,
  700. MapBase.CONTROLS.TYPE,
  701. MapBase.CONTROLS.ZOOM,
  702. ]),
  703. /**
  704. * Data that will be parsed as GeoJSONData
  705. * @review
  706. * @type {Object}
  707. */
  708. data: Config.object(),
  709. /**
  710. * If true, the geolocation API will be used (if implemented)
  711. * @review
  712. * @type {boolean}
  713. */
  714. geolocation: Config.bool().value(false),
  715. /**
  716. * Position being shown on the map. This value will be updated
  717. * if the position is changed internally.
  718. * @review
  719. * @type {{ lat: number, lng: number }}
  720. */
  721. position: Config.shapeOf({
  722. location: Config.shapeOf({
  723. lat: Config.number().value(0),
  724. lng: Config.number().value(0),
  725. }),
  726. })
  727. .value({
  728. location: {lat: 0, lng: 0},
  729. })
  730. .setter('setPosition'),
  731. /**
  732. * Zoom being used on the map.
  733. * @review
  734. * @type {number}
  735. */
  736. zoom: Config.number().value(11),
  737. };
  738. Liferay.MapBase = MapBase;
  739. export default MapBase;
  740. export {MapBase};