/*
* Copyright 2013 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 Google Map.<br>
* This dialog box embeds an address search bar and a GoogleMaps.<br>
* This creates a marker on the GoogleMaps. That marker is draggable at will.<br>
* An initial address can be provided - to initialize the searrch field - in the configuration parameter of the #open method.
*/
Ext.define('Ametys.helper.ChooseLocation', {
singleton: true,
/**
* The default zoom level
* @private
* @readonly
*/
__DEFAULT_ZOOM_LEVEL: 17,
/**
* 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;
// Fetch the API key for google maps and load the script
Ametys.helper.ChooseLocation.LoadGoogleMaps.getDefaultAPIKey(Ext.bind(this._getAPIKeyCb, this, [config], 1));
},
/**
* @private
* Callback invoked once the api key is retrieved
* @param {String} apiKey the Google API key
* @param {Object} config The configuration object
*/
_getAPIKeyCb: function (apiKey, config)
{
Ametys.helper.ChooseLocation.LoadGoogleMaps.loadScript(apiKey,
Ext.bind(
function(){
this._delayedInitialize(config, apiKey),
this._gmapwindow.show();
this._gmapwindow.down('#geo-search-textfield').focus();
},
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
* @param {String} [apiKey] The Google Api key
*/
_delayedInitialize: function(config, apiKey)
{
// Initial position of marker
var initialLatLng = config.initialLatLng ? new google.maps.LatLng(config.initialLatLng.latitude, config.initialLatLng.longitude) : null;
var defaultLatLng = config.defaultLatLng ? new google.maps.LatLng(config.defaultLatLng.latitude, config.defaultLatLng.longitude) : new google.maps.LatLng(0.0, 0.0);
var initialAddress = config.initialAddress;
// Zoom level
var zoomLevel = initialLatLng || initialAddress ? (config.zoomLevel || this.__DEFAULT_ZOOM_LEVEL) : (defaultLatLng ? (config.defaultZoomLevel || 6) : 1);
// Create the GMap panel
this._gmapPanel = Ext.create('Ext.ux.GMapPanel', {
name : 'gmappanel',
flex: 1,
mapOptions: {
mapTypeId: config.mapTypeId || google.maps.MapTypeId.HYBRID,
zoom: zoomLevel,
zoomControl: true,
zoomControlOptions: {
style: config.zoomControlStyle || google.maps.ZoomControlStyle.LARGE
}
},
center : new google.maps.LatLng(0.0, 0.0),
listeners: {
mapready: Ext.bind (this._pinInitialMarker, this, [initialLatLng, config.initialAddress, defaultLatLng], 2)
}
});
var me = this;
this._searchErrorMsg = '';
this._gmapwindow = 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-warning',
html: "{{i18n PLUGINS_CORE_UI_GEOCODE_GMAP_DIALOG_MISSING_API_KEY}}",
hidden: !Ext.isEmpty(apiKey)
},
{
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,
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._gmapPanel,
{
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),
disabled: Ext.isEmpty(apiKey)
}, {
text :'{{i18n PLUGINS_CORE_UI_GEOCODE_GMAP_DIALOG_CANCEL}}',
handler: Ext.bind(this._cancel, this)
}
],
listeners: {
close: Ext.bind(this._onClose, this)
}
});
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._gmapwindow.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 {Ext.ux.GMapPanel} gmapPanel The GMap panel
* @param {google.maps.Map} gmap The Google Map
* @param {google.maps.LatLng} initialLatLng The initial position of the marker. Can be null to center the map to the initial address.
* @param {String} initialAddress The (postal) address to center the map. Can be null to center the map to the default position.
* @param {google.maps.LatLng} defaultLatLng The default position of the marker if initialLatLng and initialAddress are empty (default to : 0°N 0°E)
*/
_pinInitialMarker: function (gmapPanel, gmap, 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._gmapwindow.down('#geo-search-textfield').getValue();
if (textfieldAddress)
{
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(new google.maps.LatLng(latitude, longitude));
},
/**
* Moves the marker at a given (postal) address
* @param {String} anAddress A string representing a (postal) address
*/
pinAtAddress: function(anAddress)
{
// GoogleMaps method to convert an address into a location
var geocoder = new google.maps.Geocoder();
var geocoderRequest = {
address : anAddress
};
var me = this;
geocoder.geocode(geocoderRequest, function (geocoderResults, geocoderStatus){
if (geocoderStatus === 'OK')
{
// On a successful address->location translation, move the marker to that location
var location = geocoderResults[0].geometry.location;
var latLng = new google.maps.LatLng(location.lat(), location.lng());
me._pinAtLatLng(latLng);
me._cleanErrorSearchMessage();
}
else
{
me._setErrorSearchMessage("{{i18n PLUGINS_CORE_UI_GEOCODE_GMAP_DIALOG_SEARCH_NOT_FOUND}}" + anAddress);
}
});
},
/**
* Center the map at a given latLng
* @param {google.maps.LatLng} latLng The new location of the center of the map
*/
centerMap: function(latLng)
{
if (this._gmapPanel.gmap)
{
if(latLng)
{
this._gmapPanel.gmap.setCenter(latLng);
}
else
{
var defaultLocation = new google.maps.LatLng(0, 0);
this._gmapPanel.gmap.setCenter(defaultLocation);
}
}
},
/**
* @private
* Move the marker at a given latLng and center the map
* @param {google.maps.LatLng} latLng The new location of the marker
*/
_pinAtLatLng: function(latLng)
{
this._removeExistingMarker();
this.centerMap(latLng);
this._newPinAt(latLng, Ext.bind(this._saveLatLng, this));
},
/**
* @private
* Save a latLng in the internal _currentLatLng
* @param {google.maps.LatLng} latLng The location to save
*/
_saveLatLng: function (latLng)
{
this._currentLatLng = latLng;
},
/**
* @private
* Remove the existing marker from the map.
*/
_removeExistingMarker: function()
{
if (this.locationMarker)
{
this.locationMarker.setMap(null);
}
},
/**
* @private
* Adds a marker at a given location on the map. The marker is draggable.
* @param {google.maps.LatLng} latLng The location of the newly created marker.
* @param {Function} onMove The callback after a marker's drag/drop.
*/
_newPinAt: function(latLng, onMove)
{
function onDragEnd (event)
{
// Save position on move
this._saveLatLng (event.latLng);
};
// Setup marker at position 'latLng'
var locationMarker = new google.maps.Marker({
position : latLng,
title : "",
draggable : true,
listeners : {
dragend : Ext.bind(onDragEnd, this)
}
});
// Add marker to map and save it
this.locationMarker = this._gmapPanel.addMarker(locationMarker);
// Save position
this._saveLatLng(latLng)
},
/**
* @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
if (Ext.isFunction (this._cbFn) && this._currentLatLng)
{
var value = {
latitude: this._currentLatLng.lat(),
longitude: this._currentLatLng.lng()
};
this._cbFn(value);
}
if (this.locationMarker)
{
// Forget about the location marker
this.locationMarker.setMap(null);
this.locationMarker = null;
}
// Close the window
this._validated = true;
this._gmapwindow.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.locationMarker)
{
// Forget about the location marker
this.locationMarker.setMap(null);
this.locationMarker = null;
}
// Close the window
this._gmapwindow.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)
{
if (!this._validated && this._cbFn)
{
// Call the callback with no value
this._cbFn(null);
}
}
});