/*
* Copyright 2019 Anyware Services
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* This UI helper provides a dialog to select coordinates on a Map, using Leaflet.<br>
* This dialog box embeds an address search bar and a Map.<br>
* This creates a marker on the map. That marker is draggable at will.<br>
* An initial address can be provided - to initialize the search field - in the configuration parameter of the #open method.
*/
Ext.define('Ametys.helper.ChooseLocationLeaflet', {
singleton: true,
/**
* The default zoom level
* @private
* @readonly
*/
__DEFAULT_ZOOM_LEVEL: 17,
/**
* The default map center
* @private
* @readonly
*/
__DEFAULT_MAP_CENTER: [0.0, 0.0],
/**
* The default map tiles url
* @private
* @readonly
*/
__DEFAULT_TILES_URL: "https://{s}.tile.osm.org/{z}/{x}/{y}.png",
/**
* The default map attribution
* @private
* @readonly
*/
__DEFAULT_MAP_ATTRIBUTION: '© <a href="https://osm.org/copyright">OpenStreetMap</a> contributors',
/**
* The regexp to check if a given address is coordinates
* @private
* @readonly
*/
__ADDRESS_COORDINATES: /^((\\+|-)?[0-9]+(\.[0-9]+)?,( )?(\\+|-)?[0-9]+(\.[0-9]+)?)$/,
/**
* The marker displayed on the map
* @type {L.marker}
* @private
*/
_marker: null,
/**
* The Leaflet map
* @type {L.map}
* @private
*/
_map: null,
/**
* The Leaflet geocoder
* @type {L.Control.geocoder}
* @private
*/
_geocoder: null,
/**
* Allow the user to setup a GoogleMaps marker
* @param {Object} [config] The initial parameters of the widget.
* @param {Object} [config.zoomLevel] The default zoom level.
* @param {String} [config.mapTypeId=google.maps.MapTypeId#HYBRID] The map type. Defaults to hybrid (roadmap + satellite)
* @param {String} [config.zoomControlStyle=google.maps.ZoomControlStyle.LARGE] The zoom control style.
* @param {Object} [config.initialLatLng] The initial position of the marker :
* @param {Number} [config.initialLatLng.latitude] The initial latitude of the marker.
* @param {Number} [config.initialLatLng.longitude] The initial longitude of the marker.
* @param {String} [config.initialAddress] The initial value of the address textfield. If config.initialLatLng is not provided in the config object, then this string also setups the initial position of the marker.
* @param {String} [config.helpMessage] The help message to display at the top of the window
* @param {String} [config.icon] The full icon path of the dialog box
* @param {String} [config.title] The title of the dialog box
* @param {Function} [callback] The method that will be called when the dialog box is closed. The method signature is :
* @param {Object} [callback.latlng] The chosen latitude and longitude
* @param {Number} [callback.latlng.latitude] The chosen latitude
* @param {Number} [callback.latlng.longitude] The chosen longitude
*/
open: function (config, callback)
{
this._cbFn = callback;
this._validated = false;
this._mapPanel = Ext.create('Ext.Component', {
name : 'mappanel',
flex: 1
});
var me = this;
this._searchErrorMsg = '';
this._mapwindow = Ext.create("Ametys.window.DialogBox",{
title: config.title || "{{i18n PLUGINS_CORE_UI_GEOCODE_GMAP_DIALOG_DEFAULT_TITLE}}",
icon: config.icon || Ametys.getPluginResourcesPrefix('cms') + '/img/widgets/geolocation/geolocation_16.png',
closeAction : 'destroy',
width : 550,
height : 450,
layout: 'fit',
items: {
xtype: 'panel',
layout: {
type: 'vbox',
align : 'stretch',
pack : 'start'
},
border: false,
items : [
{
xtype: 'component',
cls: 'a-text',
html: config.helpMessage || "{{i18n PLUGINS_CORE_UI_GEOCODE_GMAP_DIALOG_HELP_MSG_1}}"
},
{
// Search address bar
xtype: 'panel',
layout: {
type: 'hbox',
align: 'stretch'
},
height: 26,
border: false,
items: [
{
xtype: 'textfield',
id: 'geo-search-textfield',
fieldLabel: "{{i18n PLUGINS_CORE_UI_GEOCODE_GMAP_DIALOG_SEARCH_LABEL}}",
labelAlign: 'right',
labelSeparator: '',
labelWidth: 80,
value: config.initialAddress.replace(/\+/g, ' '),
flex: 1,
listeners: {
specialkey: Ext.bind(this._pinAddressOnEnterKeyPress, this)
}
},
{
xtype: 'button',
iconCls: 'ametysicon-magnifier12',
tooltip : "{{i18n PLUGINS_CORE_UI_GEOCODE_GMAP_DIALOG_SEARCH}}",
handler : Ext.bind(this._pinAtTextfieldAddress, this)
}
]
},
{
xtype: 'component',
cls: 'a-text-error',
itemId: 'geo-search-textfield-error',
html: me._searchErrorMsg || ''
},
this._mapPanel,
{
xtype: 'component',
cls: 'a-text',
html: config.bottomText || ""
}
]
},
defaultButton: 'okButton',
referenceHolder: true,
// Buttons
buttons : [{
reference: 'okButton',
text :"{{i18n PLUGINS_CORE_UI_GEOCODE_GMAP_DIALOG_OK}}",
handler: Ext.bind(this._ok, this)
}, {
text :'{{i18n PLUGINS_CORE_UI_GEOCODE_GMAP_DIALOG_CANCEL}}',
handler: Ext.bind(this._cancel, this)
}
],
listeners: {
close: Ext.bind(this._onClose, this),
show: Ext.bind(this._loadLeafLet, this, [config])
}
});
this._mapwindow.fireDefaultButton = function(e)
{
if (e.target.tagName === 'INPUT')
{
return true;
}
return Ametys.window.DialogBox.prototype.fireDefaultButton.apply(this, arguments);
}
Ametys.loadStyle(Ametys.getPluginDirectPrefix('leaflet') + "/resources/css/leaflet.css");
Ametys.loadStyle(Ametys.getPluginDirectPrefix('leaflet-control-geocoder') + "/resources/css/Control.Geocoder.css");
this._mapwindow.show();
},
/**
* Load leaflet map and geocoder
* @param {Object} [config] The initial parameters of the widget. (see "open")
* @private
*/
_loadLeafLet: function(config)
{
Ametys.loadScript(Ametys.getPluginDirectPrefix('leaflet') + "/resources/js/leaflet.js",
Ext.bind(
function(){
this._loadLeafLetGeoCoder(config);
},
this)
);
},
/**
* Load leaflet geocoder
* @param {Object} [config] The initial parameters of the widget. (see "open")
* @private
*/
_loadLeafLetGeoCoder: function(config)
{
Ametys.loadScript(Ametys.getPluginDirectPrefix('leaflet-control-geocoder') + "/resources/js/Control.Geocoder.js",
Ext.bind(
function(){
this._delayedInitialize(config);
},
this)
);
},
/**
* Creates the dialog box if it is not already created and initialize it
* @param {Object} [config] The dialog box configuration object
* @param {Object} [config.initialLatLng] The initial position of the marker.
* @param {Object} [config.defaultLatLng] The default position of the marker when there is no initial address.
* @param {Number} [config.defaultZoomLevel=6] The default zoom level when for default latitude/longitude
* @param {Number} config.initialLatLng.latitude The initial latitude of the marker.
* @param {Number} config.initialLatLng.longitude The initial longitude of the marker.
* @param {String} [config.initialAddress] The initial value of the address textfield. If config.initialLatLng is not provided in the config object, then this string also setups the initial position of the marker.
* @param {String} [config.helpMessage] The help message to display at the top of the window
* @param {String} [config.icon] The relative path of the window icon
* @param {String} [config.title] The title of the window
*/
_delayedInitialize: function(config)
{
// Initial position of marker
var initialLatLng = config.initialLatLng ? L.latLng(config.initialLatLng.latitude, config.initialLatLng.longitude) : null;
var defaultLatLng = config.defaultLatLng ? L.latLng(config.defaultLatLng.latitude, config.defaultLatLng.longitude) : L.latLng(0.0, 0.0);
var initialAddress = config.initialAddress.replace(/\+/g, ' ');
// Zoom level
var zoomLevel = initialLatLng || initialAddress ? (config.zoomLevel || this.__DEFAULT_ZOOM_LEVEL) : (defaultLatLng ? (config.defaultZoomLevel || 6) : 1);
this._map = L.map(this._mapPanel.getId()).setZoom(zoomLevel);
L.tileLayer(this.__DEFAULT_TILES_URL, {
attribution: this.__DEFAULT_MAP_ATTRIBUTION
}).addTo(this._map);
var geoCoderOptions = {
collapsed: false, // Opened by default
showResultIcons: true, // Display custom icons in the search results
defaultMarkGeocode: false // Custom marker
};
var geocoder = L.Control.geocoder(geoCoderOptions);
var me = this;
geocoder.on('markgeocode', function(e) {
me._pinAtLatLngAndZoom(e.geocode);
});
//.addTo(this._map);
this._geocoder = geocoder.options.geocoder;
this._pinInitialMarker(initialLatLng, initialAddress, defaultLatLng);
return true;
},
/**
* Set the error message under the search field
* @param {String} [msg] The message to display, or undefined/null to remove the message.
* @private
*/
_setErrorSearchMessage: function(msg)
{
this._searchErrorMsg = msg || '';
var searchErrorCmp = this._mapwindow.down('#geo-search-textfield-error');
if (searchErrorCmp)
{
searchErrorCmp.update(this._searchErrorMsg);
}
},
/**
* Remove the error message under the search field
* @private
*/
_cleanErrorSearchMessage: function()
{
this._setErrorSearchMessage();
},
/**
* Search for the address on the map whenever you type Enter in the search address textfield
* @param {Ext.form.field.Text} input The input text
* @param {Ext.event.Event} e The event object
* @private
*/
_pinAddressOnEnterKeyPress: function(input, e)
{
if (e.getKey() == e.ENTER)
{
e.preventDefault();
e.stopPropagation();
this._pinAtTextfieldAddress();
}
},
/**
* @private
* Pin the marker to the initial position.
* @param {L.latLng|Number[]} initialLatLng
* @param {String} initialAddress The (postal) address to center the map. Can be null to center the map to the default position.
* @param {L.latLng|Number[]} defaultLatLng The default position of the marker if initialLatLng and initialAddress are empty (default to : 0°N 0°E)
*/
_pinInitialMarker: function (initialLatLng, initialAddress, defaultLatLng)
{
if (initialLatLng)
{
this._pinAtLatLng(initialLatLng);
}
else if (initialAddress)
{
this.pinAtAddress(initialAddress);
}
else
{
this._pinAtLatLng(defaultLatLng);
}
},
/**
* @private
* Moves the marker at the location described in the address search bar
*/
_pinAtTextfieldAddress: function()
{
var textfieldAddress = this._mapwindow.down('#geo-search-textfield').getValue();
if (textfieldAddress)
{
if (this.__ADDRESS_COORDINATES.test(textfieldAddress))
{
var coordinatesArray = textfieldAddress.split(",");
this._pinAtLatLng(coordinatesArray);
this._map.fitBounds([
coordinatesArray,
coordinatesArray
]);
this._cleanErrorSearchMessage();
}
else
{
this.pinAtAddress(textfieldAddress);
}
//this._gmapPanel.gmap.setZoom(this.__DEFAULT_ZOOM_LEVEL);
}
},
/**
* Moves the marker at a given latitude/longitude
* @param {Number} latitude The latitude to setup the marker
* @param {Number} longitude The longitude to setup the marker
*/
pinAtLatitudeLongitude: function(latitude, longitude)
{
this._pinAtLatLng([latitude, longitude]);
},
/**
* Moves the marker at a given (postal) address
* @param {String} anAddress A string representing a (postal) address
*/
pinAtAddress: function(anAddress)
{
var geocoderRequest = [anAddress];
var me = this;
this._geocoder.geocode(geocoderRequest, function (geocoderResults){
if (geocoderResults && geocoderResults.length > 0)
{
// On a successful address->location translation, move the marker to that location
var firstResult = geocoderResults[0];
me._pinAtLatLngAndZoom(firstResult);
me._cleanErrorSearchMessage();
}
else
{
me._setErrorSearchMessage("{{i18n PLUGINS_CORE_UI_GEOCODE_GMAP_DIALOG_SEARCH_NOT_FOUND}}" + anAddress);
}
});
},
/**
* Center the map at a given latLng
* @param {L.latLng|Number[]} latLng The new location of the center of the map
*/
centerMap: function(latLng)
{
if (this.map)
{
if(latLng)
{
this._map.panTo(geocode.center);
}
else
{
var defaultLocation = [0, 0];
this._map.panTo(defaultLocation);
}
}
},
/**
* @private
* Move the marker at a given latLng and center the map
* @param {L.latLng|Number[]} latLng The new location of the marker
*/
_pinAtLatLng: function(latLng)
{
if (this._marker == null)
{
var markerOptions = {
draggable: true
};
this._marker = L.marker(latLng, markerOptions).addTo(this._map);
}
else
{
this._marker.setLatLng(latLng);
}
this._map.panTo(latLng);
},
/**
* @private
* Move the marker at a given latLng and center the map
* @param {L.Control.Geocoder.GeocodingResult} result of a search, contains the center and also the bbox to zoom properly
*/
_pinAtLatLngAndZoom: function(geocode)
{
this._pinAtLatLng(geocode.center);
var bbox = geocode.bbox;
var poly = L.polygon([
bbox.getSouthEast(),
bbox.getNorthEast(),
bbox.getNorthWest(),
bbox.getSouthWest()
]);
this._map.fitBounds(poly.getBounds());
},
/**
* @private
* Returns the position of the current marker
*/
_getMarkerLatLng: function()
{
if (this._marker)
{
return this._marker.getLatLng();
}
else
{
return null;
}
},
/**
* @private
* Function called when pressing the 'ok' button of the dialog box.<br>
* Calls the user-defined callback and closes the dialog box.
*/
_ok: function()
{
// Call the callback with the current latLng
var currentLatLng = this._getMarkerLatLng();
if (Ext.isFunction (this._cbFn) && currentLatLng != null)
{
var value = {
latitude: currentLatLng.lat,
longitude: currentLatLng.lng
};
this._cbFn(value);
}
if (this._marker)
{
// Forget about the location marker
this._marker.remove();
this._marker = null;
}
// Close the window
this._validated = true;
this._mapwindow.close();
},
/**
* @private
* Function called when pressing the 'cancel' button of the dialog box.<br>
* Calls the user-defined callback with no arguements and closes the dialog box.
*/
_cancel: function()
{
if (this._marker)
{
// Forget about the location marker
this._marker.remove();
this._marker = null;
}
// Close the window
this._mapwindow.close();
},
/**
* @private
* Listener when closing the dialog box
* @param {Boolean} validate true the dialog box was closing after cliking on 'Ok' button
*/
_onClose: function(validate)
{
this._map.remove();
if (!this._validated && this._cbFn)
{
// Call the callback with no value
this._cbFn(null);
}
}
});