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 'frontend-js-web';
  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) =>
  310. feature.getGeometry().get()
  311. );
  312. if (locations.length > 1) {
  313. locations.forEach((location) => bounds.extend(location));
  314. }
  315. else {
  316. this.position = {location: locations[0]};
  317. }
  318. }
  319. /**
  320. * Event handler executed when a GeoJSONLayer feature is clicked. It
  321. * simple propagates the event with the given feature.
  322. * @param {{ feature: Object }} param0 Feature to be propagated.
  323. * @protected
  324. * @review
  325. */
  326. _handleGeoJSONLayerFeatureClicked({feature}) {
  327. this.emit('featureClick', {feature});
  328. }
  329. /**
  330. * Event handler executed when the geolocation marker has been dragged to
  331. * a new position. It updates the instance position.
  332. * @param {{ location: Object }} param0 New marker location.
  333. * @protected
  334. * @review
  335. * @see MapBase._getGeoCoder()
  336. * @see MapBase.position
  337. */
  338. _handleGeoLocationMarkerDragended({location}) {
  339. this._getGeocoder().reverse(location, ({data}) => {
  340. this.position = data;
  341. });
  342. }
  343. /**
  344. * Event handler executed when the home button GeoJSONLayer feature is
  345. * clicked. It resets the instance position to _originalPosition and stops
  346. * the event propagation.
  347. * @param {Event} event Click event.
  348. * @review
  349. * @see MapBase.position
  350. * @see MapBase._originalPosition
  351. */
  352. _handleHomeButtonClicked(event) {
  353. event.preventDefault();
  354. this.position = this._originalPosition;
  355. }
  356. /**
  357. * Event handler executed when the user position changes. It centers the map
  358. * and, if existing, updates the _geolocationMarker position.
  359. * @param {{ newVal: { location: Object } }} param0 New location information.
  360. * @review
  361. * @see MapBase.setCenter()
  362. * @see MapBase._geolocationMarker
  363. */
  364. _handlePositionChanged({newVal: {location}}) {
  365. this.setCenter(location);
  366. if (this._geolocationMarker) {
  367. this._geolocationMarker.setPosition(location);
  368. }
  369. }
  370. /**
  371. * Event handler executed when the search button GeoJSONLayer feature has
  372. * been clicked. It updates the instance position.
  373. * @param {{ position: Object }} param0 New position.
  374. * @review
  375. * @see MapBase.position
  376. */
  377. _handleSearchButtonClicked({position}) {
  378. this.position = position;
  379. }
  380. /**
  381. * If this.data has a truthy value and this._geoJSONLayer has been
  382. * set, it tries to parse geoJSON information with _geoJSONLayer.addData()
  383. * @protected
  384. * @review
  385. * @see this._geoJSONLayer
  386. * @see this.data
  387. */
  388. _initializeGeoJSONData() {
  389. if (this.data && this._geoJSONLayer) {
  390. this._geoJSONLayer.addData(this.data);
  391. }
  392. }
  393. /**
  394. * Initializes the instance map using the given location and, if
  395. * this.geolocation property is true, tries to get the location using
  396. * the geocoder.
  397. * @param {Object} location Location object user for map initialization.
  398. * @protected
  399. * @review
  400. * @see this._initializeMap()
  401. * @see this._getGeocoder()
  402. */
  403. _initializeLocation(geolocation) {
  404. const geocoder = this._getGeocoder();
  405. if (this.geolocation && geocoder) {
  406. geocoder.reverse(geolocation, ({data}) =>
  407. this._initializeMap(data)
  408. );
  409. }
  410. else {
  411. this._initializeMap({location: geolocation});
  412. }
  413. }
  414. /**
  415. * Creates a new map with the given position and initialize the map
  416. * controls, loads the geoJSONData and, if geolocation is active, creates
  417. * a marker for it.
  418. * @param {Object} position Initial position added to the map.
  419. * @protected
  420. * @review
  421. * @see MapBase._getControlsConfig()
  422. * @see MapBase._createMap()
  423. * @see MapBase.GeoJSONImpl
  424. * @see MapBase.addMarker()
  425. * @see MapBase._createCustomControls()
  426. * @see MapBase._bindUIMB()
  427. * @see MapBase._initializeGeoJSONData()
  428. */
  429. _initializeMap(position) {
  430. const controlsConfig = this._getControlsConfig();
  431. const geolocation = position.location;
  432. this._originalPosition = position;
  433. this._map = this._createMap(geolocation, controlsConfig);
  434. if (
  435. this.constructor.GeoJSONImpl &&
  436. this.constructor.GeoJSONImpl !== GeoJSONBase
  437. ) {
  438. this._geoJSONLayer = new this.constructor.GeoJSONImpl({
  439. map: this._map,
  440. });
  441. }
  442. if (this.geolocation) {
  443. this._geolocationMarker = this.addMarker(geolocation);
  444. }
  445. this.position = position;
  446. this._createCustomControls();
  447. this._bindUIMB();
  448. this._initializeGeoJSONData();
  449. }
  450. /**
  451. * Adds a new control to the interface at the given position.
  452. * @param {Object} control Native control object
  453. * @param {MapBase.POSITION} position Position defined in MapBase class
  454. * @review
  455. */
  456. addControl(
  457. /* eslint-disable no-unused-vars */
  458. control,
  459. position
  460. /* eslint-enable no-unused-vars */
  461. ) {
  462. throw new Error('This method must be implemented');
  463. }
  464. /**
  465. * Returns the map bounds.
  466. * @abstract
  467. * @return {Object} Map bounds
  468. * @review
  469. */
  470. getBounds() {
  471. throw new Error('This method must be implemented');
  472. }
  473. /**
  474. * Centers the map on the given location.
  475. * @abstract
  476. * @param {Object} location
  477. * @review
  478. */
  479. setCenter(
  480. /* eslint-disable no-unused-vars */
  481. location
  482. /* eslint-enable no-unused-vars */
  483. ) {
  484. throw new Error('This method must be implemented');
  485. }
  486. /**
  487. * If the class MapBase.MarkerImpl has been specified, creates an instance
  488. * of this class with the given location and the existing this._map object
  489. * and returns it.
  490. * @param {Object} location Location object used for the marker position
  491. * @return {MapBase.MarkerImpl}
  492. * @review
  493. */
  494. addMarker(location) {
  495. let marker;
  496. if (
  497. this.constructor.MarkerImpl &&
  498. this.constructor.MarkerImpl !== MarkerBase
  499. ) {
  500. marker = new this.constructor.MarkerImpl({
  501. location,
  502. map: this._map,
  503. });
  504. }
  505. return marker;
  506. }
  507. /**
  508. * Returns the stored map
  509. * @return {Object}
  510. * @review
  511. * @see MapBase._initializeMap()
  512. */
  513. getNativeMap() {
  514. return this._map;
  515. }
  516. /**
  517. * Adds a listener for the given event using the given context. This
  518. * methods uses MetalJS' functionality, but overrides the context binding
  519. * in order to avoid breaking changes with the old implementation.
  520. * @param {string} eventName Event name that will be listened.
  521. * @param {function} callback Callback executed when the event fires.
  522. * @param {Object} [context] Optional context that will be used for binding
  523. * the callback when specified.
  524. * @return {*} Result of the State.on method.
  525. * @review
  526. * @see State.on
  527. */
  528. on(eventName, callback, context) {
  529. let boundCallback = callback;
  530. if (context) {
  531. boundCallback = callback.bind(context);
  532. }
  533. return super.on(eventName, boundCallback);
  534. }
  535. /**
  536. * Opens a dialog if this_getDialog() returns a valid object.
  537. * @param {*} dialogConfig Dialog configuration that will be sent to the
  538. * dialog.open() method.
  539. * @review
  540. * @see MapBase._getDialog()
  541. */
  542. openDialog(dialogConfig) {
  543. const dialog = this._getDialog();
  544. if (dialog) {
  545. dialog.open(dialogConfig);
  546. }
  547. }
  548. /**
  549. * Setter called everytime the position attribute is changed.
  550. * @param {Object} position New position
  551. * @return {Object} The given position object
  552. * @review
  553. */
  554. setPosition(position) {
  555. this.emit('positionChange', {
  556. newVal: {
  557. address: position.address,
  558. location: position.location,
  559. },
  560. });
  561. return position;
  562. }
  563. }
  564. /**
  565. * Registers the given callback to be executed when the map identified
  566. * by the given id has been created. If the map has already been created, it
  567. * is executed immediatly.
  568. * @param {string} id Id of the map that needs to be created
  569. * @param {function} callback Callback being executed
  570. * @review
  571. */
  572. MapBase.get = function (id, callback) {
  573. const map = Liferay.component(id);
  574. if (map) {
  575. callback(map);
  576. }
  577. else {
  578. const idPendingCallbacks = pendingCallbacks[id] || [];
  579. idPendingCallbacks.push(callback);
  580. pendingCallbacks[id] = idPendingCallbacks;
  581. }
  582. };
  583. /**
  584. * Registers the given map with the given id, and executes all existing
  585. * callbacks associated with the id. Then it clears the list of callbacks.
  586. * @param {string} id Id of the map that has been created
  587. * @param {Object} map Map that has been created
  588. * @param {string} portletId Id of the portlet that registers the map
  589. * @review
  590. */
  591. MapBase.register = function (id, map, portletId) {
  592. const componentConfig = portletId ? {portletId} : {destroyOnNavigate: true};
  593. Liferay.component(id, map, componentConfig);
  594. const idPendingCallbacks = pendingCallbacks[id];
  595. if (idPendingCallbacks) {
  596. idPendingCallbacks.forEach((callback) => callback(map));
  597. idPendingCallbacks.length = 0;
  598. }
  599. };
  600. /**
  601. * Class that will be used for creating dialog objects.
  602. * @review
  603. */
  604. MapBase.DialogImpl = null;
  605. /**
  606. * Class that will be used for parsing geoposition information.
  607. * @review
  608. */
  609. MapBase.GeocoderImpl = null;
  610. /**
  611. * Class that will be used for creating GeoJSON instances.
  612. * This class must be replaced by another one extending GeoJSONBase.
  613. * @review
  614. */
  615. MapBase.GeoJSONImpl = GeoJSONBase;
  616. /**
  617. * Class that will be used for creating map markers.
  618. * This class must be replaced by another one extending MarkerBase.
  619. * @review
  620. */
  621. MapBase.MarkerImpl = MarkerBase;
  622. /**
  623. * Class that will be used for creating an instance of searchbox.
  624. * This class must be replaced by another one extending MarkerBase.
  625. * @review
  626. */
  627. MapBase.SearchImpl = null;
  628. /**
  629. * List of controls that maybe shown inside the rendered map.
  630. * @review
  631. */
  632. MapBase.CONTROLS = {
  633. ATTRIBUTION: 'attribution',
  634. GEOLOCATION: 'geolocation',
  635. HOME: 'home',
  636. OVERVIEW: 'overview',
  637. PAN: 'pan',
  638. ROTATE: 'rotate',
  639. SCALE: 'scale',
  640. SEARCH: 'search',
  641. STREETVIEW: 'streetview',
  642. TYPE: 'type',
  643. ZOOM: 'zoom',
  644. };
  645. /**
  646. * Control mapping that should be overriden by child classes.
  647. * @review
  648. */
  649. MapBase.CONTROLS_MAP = {};
  650. /**
  651. * Available map positions.
  652. * @review
  653. */
  654. MapBase.POSITION = {
  655. BOTTOM: 11,
  656. BOTTOM_CENTER: 11,
  657. BOTTOM_LEFT: 10,
  658. BOTTOM_RIGHT: 12,
  659. CENTER: 13,
  660. LEFT: 5,
  661. LEFT_BOTTOM: 6,
  662. LEFT_CENTER: 4,
  663. LEFT_TOP: 5,
  664. RIGHT: 7,
  665. RIGHT_BOTTOM: 9,
  666. RIGHT_CENTER: 8,
  667. RIGHT_TOP: 7,
  668. TOP: 2,
  669. TOP_CENTER: 2,
  670. TOP_LEFT: 1,
  671. TOP_RIGHT: 3,
  672. };
  673. /**
  674. * Position mapping that should be overriden by child classes.
  675. * @review
  676. */
  677. MapBase.POSITION_MAP = {};
  678. /**
  679. * State definition.
  680. * @review
  681. * @static
  682. * @type {!Object}
  683. */
  684. MapBase.STATE = {
  685. /**
  686. * DOM node selector identifying the element that will be used
  687. * for rendering the map
  688. * @review
  689. * @type {string}
  690. */
  691. boundingBox: Config.string().value(''),
  692. /**
  693. * List of controls that will be shown on the map.
  694. * The full control list is kept inside MapBase.CONTROLS.
  695. * @review
  696. * @type {Array<string>}
  697. */
  698. controls: Config.validator(
  699. isSubsetOf(Object.values(MapBase.CONTROLS))
  700. ).value([
  701. MapBase.CONTROLS.PAN,
  702. MapBase.CONTROLS.TYPE,
  703. MapBase.CONTROLS.ZOOM,
  704. ]),
  705. /**
  706. * Data that will be parsed as GeoJSONData
  707. * @review
  708. * @type {Object}
  709. */
  710. data: Config.object(),
  711. /**
  712. * If true, the geolocation API will be used (if implemented)
  713. * @review
  714. * @type {boolean}
  715. */
  716. geolocation: Config.bool().value(false),
  717. /**
  718. * Position being shown on the map. This value will be updated
  719. * if the position is changed internally.
  720. * @review
  721. * @type {{ lat: number, lng: number }}
  722. */
  723. position: Config.shapeOf({
  724. location: Config.shapeOf({
  725. lat: Config.number().value(0),
  726. lng: Config.number().value(0),
  727. }),
  728. })
  729. .value({
  730. location: {lat: 0, lng: 0},
  731. })
  732. .setter('setPosition'),
  733. /**
  734. * Zoom being used on the map.
  735. * @review
  736. * @type {number}
  737. */
  738. zoom: Config.number().value(11),
  739. };
  740. Liferay.MapBase = MapBase;
  741. export default MapBase;
  742. export {MapBase};