Giscuit
Prev Next

Creating your first plugin

Introduction

One 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.

  1. define('PRODUCTION'false);

Lets give our plugin a name "Borders".

You can download the code used in examples here.

Adding plugin initialization code

Create the main plugin file "library/Giscuit/Controller/Plugin/Borders.php". This will include our Borders.js when the map is loaded.

  1. <?php
  2. /**
  3.  * Giscuit
  4.  *
  5.  * @category   Giscuit
  6.  * @package    Giscuit_Controller
  7.  * @subpackage Plugin
  8.  */
  9.  
  10. /**
  11.  * @see Zend_Controller_Plugin_Abstract
  12.  */
  13. require_once 'Zend/Controller/Plugin/Abstract.php';
  14.  
  15. /**
  16.  * Plugin for borders identification
  17.  *  
  18.  * @category   Giscuit
  19.  * @package    Giscuit_Controller
  20.  * @subpackage Plugin
  21.  */
  22. class Giscuit_Controller_Plugin_Borders extends Giscuit_Controller_Plugin_Abstract
  23. {
  24.  
  25.     /**
  26.      * Translations for the new keywords
  27.      *
  28.      * @return array 
  29.      */
  30.     protected $_defaultTranslations array(
  31.         'borders' => 'Borders' 
  32.         'borderingCountriesWith' => 'Bordering countries with' ,
  33.         'noNeighboursFound' => 'No neighbours found'
  34.     );
  35.  
  36.     /**
  37.      * List of the javascript file to include
  38.      * 
  39.      * @return array 
  40.      */
  41.     public function getJsFilesList ()
  42.     {
  43.         return array(
  44.             'Control/Borders.js'
  45.         );
  46.     }
  47.     
  48.     /**
  49.      * List of the javascript files to include for mobile
  50.      * 
  51.      * @return array 
  52.      */
  53.     public function getMobileJsFilesList ()
  54.     {
  55.         return array(
  56.             'Mobile/Control/Borders.js'
  57.         );
  58.     }
  59.     
  60.     /**
  61.      * Install plugin
  62.      *
  63.      * Here we will make our plugin accessible to registered users only.
  64.      * This will be used to limit access to our controller that will be created (Tool_BordersController)
  65.      * and to control our button visibility in the getJsInitCode method
  66.      * of the Giscuit_Controller_Plugin_Borders class.
  67.      * Also we will set a limit parameter to the number of results returned from the database.
  68.      * 
  69.      */
  70.     public function install($newConfig
  71.     {
  72.         $rulesTable new Giscuit_Db_Table_Rules();
  73.         $data array(
  74.             'role' => 'registered',
  75.             'resource' => 'tool_borders_controller',
  76.             'privilege' => '',
  77.             'group' => 'controller'
  78.         );
  79.         $rulesTable->insert($data);
  80.         
  81.         $pluginName $this->name;
  82.         $newConfig->production->plugins->$pluginName->limit 10;
  83.         
  84.         //Add default section translations
  85.         $defaultLanguageFile GT_LANGUAGES_PATH DIR_SEP 'default.en.tmx';
  86.         $translate new Zend_Translate('tmx'$defaultLanguageFile'en');
  87.         
  88.         $newMessages array_merge($translate->getMessages()$this->_defaultTranslations);
  89.         $newTmx Giscuit_Controller_Plugin_Abstract::arrayToTmx('en'$newMessages);
  90.         file_put_contents(GT_LANGUAGES_PATH DIR_SEP 'default.en.tmx'$newTmx);
  91.     }
  92.     
  93.     /**
  94.      * Uninstall plugin
  95.      *  
  96.      */
  97.     public function uninstall($newConfig
  98.     {
  99.         $rulesTable new Giscuit_Db_Table_Rules();
  100.         $where $rulesTable->getAdapter()->quoteInto('resource = ?''tool_borders_controller');
  101.         $rulesTable->delete($where);
  102.         
  103.         //Delete translations
  104.         $defaultLanguageFile GT_LANGUAGES_PATH DIR_SEP 'default.en.tmx';
  105.         $translate new Zend_Translate('tmx'$defaultLanguageFile'en');
  106.         
  107.         $translations $translate->getMessages();
  108.         foreach ($this->_defaultTranslations as $key => $value{
  109.             unset($translations[$key]);
  110.         }
  111.         
  112.         $newTmx Giscuit_Controller_Plugin_Abstract::arrayToTmx('en'$translations);
  113.         file_put_contents(GT_LANGUAGES_PATH DIR_SEP 'default.en.tmx'$newTmx);
  114.     }
  115.  
  116.     /**
  117.      * Parse settings in form
  118.      *
  119.      * Here we can add additional configuration parameters to the plugin form.
  120.      * In this case we will add the limit parameter, setting required validators and current value.
  121.      *  
  122.      * @return boolean 
  123.      */
  124.     public function parseSettingsForm($form
  125.     {
  126.         $settings $this->getSettings();
  127.         $limit $settings['limit'];
  128.     
  129.         $form->addElement('text'$this->name '_limit'array(
  130.             'validators' => array(
  131.                 'Digits'
  132.             ,
  133.             'required' => true 
  134.             'label' => 'limit' ,
  135.             'value' => $limit
  136.         ));
  137.     
  138.         return true;
  139.     }
  140.     
  141.     /**
  142.      * Parse settings from received parameters.
  143.      * This is the place where we can save received parameters to config file
  144.      *  
  145.      * @return boolean 
  146.      */
  147.     public function parseSettingsParams($newConfig$form
  148.     {
  149.         $pluginName $this->name;
  150.     
  151.         $newConfig->production->plugins->$pluginName->limit $form->getValue($pluginName '_limit');
  152.         
  153.         return true;
  154.     }}

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 plugin

Ok, 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

  1. <?php
  2.  
  3. class Tool_BordersController extends Zend_Controller_Action
  4. {
  5.  
  6.     public function getAction ()
  7.     {
  8.         $config Zend_Registry::get('config');
  9.         $request $this->getRequest();
  10.         
  11.         //Filter input data
  12.         $lon floatval($request->getParam('lon'));
  13.         $lat floatval($request->getParam('lat'));
  14.         
  15.         $result['replyCode'200;
  16.         $result['records'array();
  17.         
  18.         //Initialize world boudnaries table
  19.         $wbTable new Giscuit_Db_Table_Layer_Worldboundaries(array(
  20.             'name' => 'world_boundaries'
  21.         ));
  22.         
  23.         //Get country and it's neighbours by providing lon and lat
  24.         $neighbours $wbTable->getNeighbours($lon$latarray(
  25.             'limit' => $config->plugins->Borders->limit
  26.         ));
  27.         
  28.         //Construct a JSON friendly array with neighbours data
  29.         if(count($neighbours0{
  30.             foreach ($neighbours as $neighbour{
  31.                 $result['records'][$neighbour->neighbour;
  32.             }
  33.             $result['name'$neighbour->name;
  34.         }
  35.                 
  36.         //Convert to JSON and send back to the client
  37.         $this->_helper->json($result);
  38.     }}

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.

  1. <?php
  2.  
  3. /**
  4.  * Giscuit
  5.  *
  6.  * @category   Giscuit
  7.  * @package    Giscuit_Db_Table
  8.  * @subpackage Layer
  9.  */
  10.  
  11. /**
  12.  * @see Giscuit_Db_Table_Layer_Abstract
  13.  */
  14. require_once 'Giscuit/Db/Table/Layer/Abstract.php';
  15.  
  16. /**
  17.  * World boundaries class for table
  18.  *
  19.  * @category   Giscuit
  20.  * @package    Giscuit_Db_Table
  21.  * @subpackage Layer
  22.  * @copyright  Copyright (c) 2012 VEC
  23.  */
  24. class Giscuit_Db_Table_Layer_Worldboundaries extends Giscuit_Db_Table_Layer_Abstract
  25. {
  26.  
  27.     /**
  28.      * Get neighbours
  29.      * 
  30.      * @param float $lon 
  31.      * @param float $lat 
  32.      * @return Zend_Db_Table_Rowset 
  33.      */
  34.     public function getNeighbours($lon$lat$options array()) {
  35.         $config Zend_Registry::get('config');
  36.         
  37.         $select $this->select();
  38.         $select->setIntegrityCheck(false);
  39.         
  40.         $select->from(
  41.             array(
  42.                 'a' => $this->_schema '.' $this->_name,
  43.             ),
  44.             'name'
  45.         );
  46.         
  47.         //Find all the polygons that touch
  48.         $select->joinInner(array(
  49.             'b' => $this->_schema '.' $this->_name
  50.         )"st_touches(a.the_geom, b.the_geom)"array(
  51.             'neighbour' => 'name'
  52.         ));
  53.  
  54.         //Find to which polygon this coordinates belong
  55.         $select->where("st_intersects('SRID={$config->map->srid};POINT($lon $lat)'::geometry, a.the_geom)");
  56.         
  57.         //Set the result limit from the plugin's parameters
  58.         if (isset($options['limit'])) {
  59.             $select->limit($options['limit']);
  60.         }
  61.                 
  62.         //Fetch the results from the database
  63.         $result $this->fetchAll($select);
  64.         
  65.         return $result;
  66.     }}

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