Creating your first plugin
IntroductionOne way to add new functionality is creating plugins.
In this tutorial we will create a new plugin that will help you identify the bordering countries of a selected country.
The main goal of this tutorial is to show you where "structure" wise you should put your code and how to interact with the platform.
In the default installation we have a world boundaries layer that has all the data we need.
I have added many comments to the examples below, please read them for better code understanding.
Before we start making our plugin we need to put our application in development mode. Open Defines.php, find PRODUCTION constant and set it to false.
Lets give our plugin a name "Borders".
You can download the code used in examples here.
Adding plugin initialization codeCreate the main plugin file "library/Giscuit/Controller/Plugin/Borders.php".
This will include our Borders.js when the map is loaded.
<?php
/**
* Giscuit
*
* @category Giscuit
* @package Giscuit_Controller
* @subpackage Plugin
*/
/**
* @see Zend_Controller_Plugin_Abstract
*/
require_once 'Zend/Controller/Plugin/Abstract.php';
/**
* Plugin for borders identification
*
* @category Giscuit
* @package Giscuit_Controller
* @subpackage Plugin
*/
class Giscuit_Controller_Plugin_Borders extends Giscuit_Controller_Plugin_Abstract
{
/**
* Translations for the new keywords
*
* @return array
*/
protected $_defaultTranslations = array(
'borders' => 'Borders' ,
'borderingCountriesWith' => 'Bordering countries with' ,
'noNeighboursFound' => 'No neighbours found'
);
/**
* List of the javascript file to include
*
* @return array
*/
public function getJsFilesList ()
{
return array(
'Control/Borders.js'
);
}
/**
* List of the javascript files to include for mobile
*
* @return array
*/
public function getMobileJsFilesList ()
{
return array(
'Mobile/Control/Borders.js'
);
}
/**
* Install plugin
*
* Here we will make our plugin accessible to registered users only.
* This will be used to limit access to our controller that will be created (Tool_BordersController)
* and to control our button visibility in the getJsInitCode method
* of the Giscuit_Controller_Plugin_Borders class.
* Also we will set a limit parameter to the number of results returned from the database.
*
*/
public function install($newConfig)
{
$rulesTable = new Giscuit_Db_Table_Rules();
$data = array(
'role' => 'registered',
'resource' => 'tool_borders_controller',
'privilege' => '',
'group' => 'controller'
);
$rulesTable->insert($data);
$pluginName = $this->name;
$newConfig->production->plugins->$pluginName->limit = 10;
//Add default section translations
$defaultLanguageFile = GT_LANGUAGES_PATH . DIR_SEP . 'default.en.tmx';
$translate = new Zend_Translate('tmx', $defaultLanguageFile, 'en');
$newMessages = array_merge($translate->getMessages(), $this->_defaultTranslations);
$newTmx = Giscuit_Controller_Plugin_Abstract::arrayToTmx('en', $newMessages);
}
/**
* Uninstall plugin
*
*/
public function uninstall($newConfig)
{
$rulesTable = new Giscuit_Db_Table_Rules();
$where = $rulesTable->getAdapter()->quoteInto('resource = ?', 'tool_borders_controller');
$rulesTable->delete($where);
//Delete translations
$defaultLanguageFile = GT_LANGUAGES_PATH . DIR_SEP . 'default.en.tmx';
$translate = new Zend_Translate('tmx', $defaultLanguageFile, 'en');
$translations = $translate->getMessages();
foreach ($this->_defaultTranslations as $key => $value) {
unset($translations[$key]);
}
$newTmx = Giscuit_Controller_Plugin_Abstract::arrayToTmx('en', $translations);
}
/**
* Parse settings in form
*
* Here we can add additional configuration parameters to the plugin form.
* In this case we will add the limit parameter, setting required validators and current value.
*
* @return boolean
*/
public function parseSettingsForm($form)
{
$settings = $this->getSettings();
$limit = $settings['limit'];
$form->addElement('text', $this->name . '_limit', array(
'validators' => array(
'Digits'
) ,
'required' => true ,
'label' => 'limit' ,
'value' => $limit
));
return true;
}
/**
* Parse settings from received parameters.
* This is the place where we can save received parameters to config file
*
* @return boolean
*/
public function parseSettingsParams($newConfig, $form)
{
$pluginName = $this->name;
$newConfig->production->plugins->$pluginName->limit = $form->getValue($pluginName . '_limit');
return true;
}}
Create "public/static/js/giscuit/Control/Borders.js" file that contains javascript object with our plugin name.
This is where all our frontend logic will reside, for now we will just alert the coordinates of the point that user clicked on the map while our control is active.
//Subscribe to draw event to initialize plugin
Giscuit.Map.prototype.gtEventListeners.push({
'draw': function() {
gt.control.borders = new Giscuit.Control.Borders();
}
});
/**
* Namespace: Giscuit.Control.Borders
*/
Giscuit.Control.Borders = OpenLayers.Class(Giscuit.Control, {
panel: null,
clickControl: null,
button: null,
initialize: function(options) {
Giscuit.Control.prototype.initialize.apply(this, [options]);
var self = this;
this.button = new YAHOO.widget.Button({
label: gt.map.getControlIcon('view_statistics.png'),
id: "gtBordersControl",
onclick: {fn: function() {
if(self.active) {
self.deactivate();
} else {
self.activate();
}}},
title: gt.translate._('borders'),
container: "gtMapControls",
type: 'checkbox'
});
},
activate: function () {
//Deactivate all active controls
gt.map.deactivateAll();
var activated = Giscuit.Control.prototype.activate.call(this);
if (activated == true) {
//Check button
this.button.set('checked', true);
//Create, add and activate a click control
this.clickControl = new Giscuit.Control.Click({
'clickCallback': Giscuit.Util.bind(this.trigger, this)
});
gt.map.addControl(this.clickControl);
this.clickControl.activate();
}
return activated;
},
deactivate: function () {
var deactivated = Giscuit.Control.prototype.deactivate.call(this);
if (deactivated == true) {
//Destroy control
this.clickControl.destroy();
this.clickControl = null;
//Uncheck button
this.button.set('checked', false);
}
return deactivated;
},
/**
* Called when user clicks on the map
*/
trigger: function (e) {
var lonlat = gt.map.getLonLatFromViewPortPx(e.xy);
//Alert where user clicked on the in lon, lat format
alert(lonlat.toString());
},
CLASS_NAME: "Giscuit.Control.Borders"
});
Time to do some testing, activate our control by clicking on it's icon in the map control section and click somewhere on the map, an alert should popup with the coordinates of the point you clicked.
Adding functionality to our pluginOk, so we have a control that collects coordinates of the point we click, now we need to send them to the server where we find the country to which this coordinates belong to and find all it's neighbours.
We will find neighbouring countries with a neat function of PostGIS st_touches.
Create a controller file application/modules/tool/controllers/BordersController.php
<?php
class Tool_BordersController extends Zend_Controller_Action
{
public function getAction ()
{
$config = Zend_Registry::get('config');
$request = $this->getRequest();
//Filter input data
$lon = floatval($request->getParam('lon'));
$lat = floatval($request->getParam('lat'));
$result['replyCode'] = 200;
$result['records'] = array();
//Initialize world boudnaries table
$wbTable = new Giscuit_Db_Table_Layer_Worldboundaries(array(
'name' => 'world_boundaries'
));
//Get country and it's neighbours by providing lon and lat
$neighbours = $wbTable->getNeighbours($lon, $lat, array(
'limit' => $config->plugins->Borders->limit
));
//Construct a JSON friendly array with neighbours data
if(count($neighbours) > 0) {
foreach ($neighbours as $neighbour) {
$result['records'][] = $neighbour->neighbour;
}
$result['name'] = $neighbour->name;
}
//Convert to JSON and send back to the client
$this->_helper->json($result);
}}
Let's follow the MVC model and create our database logic in a model class, more specifically in Worldboundaries layer table class.
The file library/Giscuit/Db/Table/Layer/Worldboundaries.php should already exist, it was created during application installation and initialization of the Worldboundaries layer.
<?php
/**
* Giscuit
*
* @category Giscuit
* @package Giscuit_Db_Table
* @subpackage Layer
*/
/**
* @see Giscuit_Db_Table_Layer_Abstract
*/
require_once 'Giscuit/Db/Table/Layer/Abstract.php';
/**
* World boundaries class for table
*
* @category Giscuit
* @package Giscuit_Db_Table
* @subpackage Layer
* @copyright Copyright (c) 2012 VEC
*/
class Giscuit_Db_Table_Layer_Worldboundaries extends Giscuit_Db_Table_Layer_Abstract
{
/**
* Get neighbours
*
* @param float $lon
* @param float $lat
* @return Zend_Db_Table_Rowset
*/
public function getNeighbours($lon, $lat, $options = array()) {
$config = Zend_Registry::get('config');
$select = $this->select();
$select->setIntegrityCheck(false);
$select->from(
array(
'a' => $this->_schema . '.' . $this->_name,
),
'name'
);
//Find all the polygons that touch
$select->joinInner(array(
'b' => $this->_schema . '.' . $this->_name
), "st_touches(a.the_geom, b.the_geom)", array(
'neighbour' => 'name'
));
//Find to which polygon this coordinates belong
$select->where("st_intersects('SRID={$config->map->srid};POINT($lon $lat)'::geometry, a.the_geom)");
//Set the result limit from the plugin's parameters
if (isset($options['limit'])) {
$select->limit($options['limit']);
}
//Fetch the results from the database
$result = $this->fetchAll($select);
return $result;
}}
Because this is a Zend framework based application we recommend learning and using their components, this will make your life easier.
Now instead of alerting the coordinates we will use YUI Connection Manager to send an asynchronous request with our coordinates as parameters,
receive response from the server, parse it with YUI JSON and populate a left accordion with html constructed from parsed data.
Open "public/static/js/giscuit/Control/Borders.js" and make the following chages:
//Subscribe to draw event to initialize plugin
Giscuit.Map.prototype.gtEventListeners.push({
'draw': function() {
gt.control.borders = new Giscuit.Control.Borders();
}
});
/**
* Namespace: Giscuit.Control.Borders
*/
Giscuit.Control.Borders = OpenLayers.Class(Giscuit.Control, {
panel: null,
clickControl: null,
button: null,
initialize: function(options) {
Giscuit.Control.prototype.initialize.apply(this, [options]);
var self = this;
this.button = new YAHOO.widget.Button({
label: gt.map.getControlIcon('view_statistics.png'),
id: "gtBordersControl",
onclick: {fn: function() {
if(self.active) {
self.deactivate();
} else {
self.activate();
}}},
title: gt.translate._('borders'),
container: "gtMapControls",
type: 'checkbox'
});
},
activate: function () {
//Deactivate all active controls
gt.map.deactivateAll();
var activated = Giscuit.Control.prototype.activate.call(this);
if (activated == true) {
//Check button
this.button.set('checked', true);
//Create, add and activate a click control
this.clickControl = new Giscuit.Control.Click({
'clickCallback': Giscuit.Util.bind(this.trigger, this)
});
gt.map.addControl(this.clickControl);
this.clickControl.activate();
}
return activated;
},
deactivate: function () {
var deactivated = Giscuit.Control.prototype.deactivate.call(this);
if (deactivated == true) {
//Destroy control
this.clickControl.destroy();
this.clickControl = null;
//Uncheck button
this.button.set('checked', false);
}
return deactivated;
},
/**
* Called when we receive a response from the server
*/
getSuccess: function (o) {
try {
var responseText = YAHOO.lang.JSON.parse(o.responseText);
} catch(e) {
Giscuit.Util.parseFailure(o, e);
}
if (responseText.replyCode != 200) {
Giscuit.Util.invalidReplyCode(responseText);
return;
}
if(responseText.records.length > 0) {
//Construct html from received data
var html = '<ul>' + gt.translate._('borderingCountriesWith') +
' ' +
responseText.name +
': <li>' +
responseText.records.join('</li><li>') +
'</li></ul>';
} else {
var html = '<p>' + gt.translate._('noNeighboursFound') + '</p>';
}
//Populate our accordion with prepared html
gt.accord.setBody(this.getPanelIndex(), html);
},
/**
* Called when user clicks on the map
*/
trigger: function (e) {
var lonlat = gt.map.getLonLatFromViewPortPx(e.xy);
//Create a new accordion or open an existing one with a loading image
if(this.panel == null) {
gt.accord.addPanel({
label: gt.translate._('borders'),
content: this.getLoadingImage()
});
this.panel = gt.accord.getPanel(gt.accord.getPanels().length - 1);
}
gt.accord.openPanel(this.getPanelIndex());
//On success call the getSuccess method
var getCallback = {
success: this.getSuccess,
timeout: this.callbackTimeout,
failure: this.callbackFailure,
scope: this
};
//Make an async request to the server with previously collected coordinates
var request = YAHOO.util.Connect.asyncRequest(
'GET',
gt.config.httpToolPath + "/borders/get" +
"/lon/" + lonlat.lon + "/lat/" + lonlat.lat,
getCallback
);
},
CLASS_NAME: "Giscuit.Control.Borders"
});
As for mobile functionality we need to create "public/static/js/giscuit/Mobile/Control/Borders.js" file.
Notice that mobile version uses jQuery and jQuery Mobile frameworks instead of YUI, so we need to adapt the javascript code from desktop version to mobile version using these libraries.
To initialize the control all you need to do is subscribe to the map "draw" event and create the control from there.
The full code is written below:
Giscuit.Mobile.Map.prototype.gtEventListeners.push({
'draw': function() {
gt.control.borders = new Giscuit.Mobile.Control.Borders();
}
});
/**
* Namespace: Giscuit.Mobile.Control.Borders
*/
Giscuit.Mobile.Control.Borders = OpenLayers.Class(Giscuit.Control, {
clickControl: null,
initialize: function(options) {
Giscuit.Control.prototype.initialize.apply(this, [options]);
//Add button, display page and events
$(document).ready(Giscuit.Util.bind(function() {
//Create page for displaying results
var html =
'<div data-role="page" id="gtBorders" data-dom-cache="true" data-url="gtBorders">' +
'<div data-role="header">' +
'<h1>' + gt.translate._('borders') + '</h1>' +
'<a href="#gtMap" data-icon="map" data-direction="reverse" class="ui-btn-right jqm-home">' +
gt.translate._('map') +
'</a>' +
'</div>' +
'<div data-role="content">' +
'</div>' +
'</div>';
$.mobile.pageContainer.append(html);
$('#gtBorders').page();
//Append control button
$("#gtMapControls").append('<div>'
+ '<div class="ui-bar ui-bar-c" id="gtBordersButton">'
+ '<div class="gtMapControlsElImage" style="background-position: 0px 50%;"></div>'
+ '<div class="gtMapControlsElText">' + gt.translate._('borders') + '</div>'
+ '</div></div>');
//Toggle activate/deactivate
$("#gtBordersButton").click(Giscuit.Util.bind(function() {
if(this.active) {
this.deactivate();
} else {
this.activate();
}}, this));
}, this));
},
activate: function () {
//Deactivate all active controls
gt.map.deactivateAll();
//Highlight control button
$("#gtBordersButton").addClass('ui-btn-active');
//Hide map controls
$("#gtMapControls").css('display', 'none');
$("#gtToggleControlsButton").css('bottom', '5px').removeClass('ui-btn-active');
var activated = Giscuit.Control.prototype.activate.call(this);
if (activated == true) {
//Create, add and activate a click control
this.clickControl = new Giscuit.Control.Click({
'clickCallback': Giscuit.Util.bind(this.trigger, this)
});
gt.map.addControl(this.clickControl);
this.clickControl.activate();
}
return activated;
},
deactivate: function () {
var deactivated = Giscuit.Control.prototype.deactivate.call(this);
//Unhighlight control button
$("#gtBordersButton").removeClass('ui-btn-active');
if (deactivated == true) {
//Destroy control
this.clickControl.destroy();
this.clickControl = null;
}
return deactivated;
},
/**
* Called when we receive a response from the server
*/
set: function (data) {
var html = '';
if(data.records.length > 0) {
//Construct html from received data
html = '<ul>' + gt.translate._('borderingCountriesWith') +
' ' + data.name + ': <li>' +
data.records.join('</li><li>') +
'</li></ul>';
} else {
var html = '<p>' + gt.translate._('noNeighboursFound') + '</p>';
}
//Populate page content with prepared html
var content = $("#gtBorders > div[data-role='content']");
content.html(html);
//Show results page
$.mobile.changePage($('#gtBorders'), {
'changeHash': true
});
$.mobile.hidePageLoadingMsg();
},
/**
* Called when user clicks on the map
*/
trigger: function (evt) {
var lonlat = gt.map.getLonLatFromViewPortPx(evt.xy);
var url = gt.config.httpToolPath + '/borders/get/lon/' + lonlat.lon +
'/lat/' + lonlat.lat;
$.mobile.showPageLoadingMsg();
$.ajax({
'type': 'GET',
'url': url,
'success': this.set,
'error': Giscuit.Util.jqmAjaxError ,
'dataType': 'json' ,
'context': this
});
},
CLASS_NAME: "Giscuit.Mobile.Control.Borders"
});
Prev |
Up |
Next |
Developer |
Developer |
Modifying Information and Search results |
COPYRIGHT ® 2012, VEC
|
|