caml_lddlayer.js

/*  LDD mapping library
    Copyright (C) 2022 Cliff Hammett

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.

*/


function lddPlanningMapLayer(meta, file, id, title, defaultLegend, defaultColor){
    var self = this;
    self.meta = meta; // containts leaflet, map, doc [i.e. the page], tabs, pointer
    self.id = id;
    self.loadState = "Loading.";
    self.totalimpact = 0;
    self.borZoom = 12;                  // Zoom level when we focus on a single borough.
    self.defaultLegend = defaultLegend; // Starting labels for applications on maps.
    self.defaultColor = defaultColor;   // Starting color of applications on map
    self.housingTotals = [];
    self.searchSet = [];              //List of planning applications that meet filter criteria 
                                      //(used by the search list)
    self.title = title;               //Title of this layer
    self.activeInnerTab = "tabFilter";//Which of the layer's inner tabs are we using (defaults to
                                      //start with fiters
    self.state = {  ready: false,   //Is this layer ready to be interacted with?
                    active: true,   //Is this the active layer?
                    visible: true   //Is this layer visible
                 }                 
    self.appliedFilters = [];
    self.mark = [];                 //sets up list of 'marks' e.g. planning applications on map
    //self.marklabel = []
    self.file = file;         
    self.st = {lat:51.5073219, lon:-0.1276474, zoom:11}; //starting location of map
    self.colors = {lg: {base: "rgb(150,255,160)", detail: "rgb(100,205,110)", text: "rgb(50,155,60)", weight: 1},
                   dg: {base: "rgb(60,200,70)", detail: "rgb(30,150,35)", text: "rgb(0,100,5)", weight: 1},
                   lb: {base: "rgb(120,120,255)", detail: "rgb(70,70,205)", text: "rgb(20,20,155)", weight: 1},
                   o:  {base: "rgb(240,170,30)", detail: "rgb(190, 120, 15)", text: "rgb(140,70,0)", weight: 1},
                   pk: {val: "rgb(240,100,140)", detail: "rgb(190,50,90)", text: "rgb(140,15,45)", weight: 1},
                   pp: {val: "rgb(180,30,200)", detail: "rgb(130,15,150)", text: "rgb(85,0,100)", weight: 1}
                       };                               // Sets up values for color codes
    setupCtls();


    function setupCtls(){
    /* sets up all the interface controls for this layer as lists, for later processing when controls are outputted
    */
        var txtFieldCtl = { label:"Search", classname: "dropdown", idName: "txtFieldDropdown" };
        var txtFieldItems = [ {val:"descr", itemName: "Description"},
                              {val:"address", itemName: "Address"},
                              {val:"borough_ref", itemName: "BoroughID"}  ];     
        var statCtl = { label:"Status", className: "dropdown", idName: "statusDropdown" };
        var statItems = [   {val:"1", itemName:"All applications"},
                            {val:"2", itemName:"In process"},
                            {val:"3", itemName:"Completed"}         ];
        var legCtl = { label:"Legend", className: "dropdown", idName: "legDropdown" };
        var legItems = [    {val:"housingPcShift", itemName: "Change to housing balance"},
                            {val:"housingImpact", itemName: "Net housing change"} 
                       ];
        var colCtl = { label:"Colour", className: "dropdown", idName: "colDropdown" };
        var colItems = [    {val: "lg", itemName: "Light Green"},
                            {val: "dg", itemName: "Dark Green"},
                            {val: "lb", itemName: "Light Blue"},
                            {val: "o", itemName: "Orange"},
                            {val: "pk", itemName: "Pink"},
                            {val: "pp", itemName: "Purple"}
                       ];
        var sortByCtl = { label:"Order by", className: "dropdown", idName: "sortByDropdown" };
        var sortByItems = [ {val:"permission_date", itemName: "Permission Date"},
                            {val:"borough_name", itemName: "Borough"}, 
                            {val:"post_code", itemName: "Postcode"}, 
                            {val:"housingImpact", itemName: "Housing Impact"} 
                       ];
        var sortOrderCtl = { label:"", className: "dropdown", idName: "sortOrderDropdown" };
        var sortOrderItems = [   {val:"asc", itemName: "Ascending"},
                                 {val:"desc", itemName: "Descending"} 
                       ];
        var appStageCtl = {label:"Application stage", className: "dropdown", idName: "appStageDropdown"};
        var appStageItems = [ {val:"0", itemName: "Loading"}];
        var borCtl = { label:"Borough", className: "dropdown", idName: "borDropdown" };
        self.borItems = getBoroughList(self.borZoom);
        self.headerInterface = [
                        { id:"shDataHeader", 
                          html:"<p><div id='shDataHeader'><strong>Data: " + self.title + "</strong></div></p>", 
                          type:"head"},
                        { id:"shSrcHeader", 
                          html:"<p><div id='shSrcHeader'><strong>Source: London Development Database, GLA</strong></div><div class='system' id='systemMsg'></div></p>", 
                          type:"head"},
                        { id: "infoExaminedHousingType",
                          html: "<div id='infoExaminedHousingType'></div>",
                          type: "info" },
                        { id:legCtl.idName,
                          html: "<p>" + buildHTMLdropdown(legCtl, legItems),
                          value: self.defaultLegend,
                          refresh: function(){self.refreshMarks();}, 
                          type: "control"},
                        { id: colCtl.idName,
                          html: " " + buildHTMLdropdown(colCtl, colItems) + "</p>",
                          value: self.defaultColor,
                          refresh: function(){self.refreshMarks();}, 
                          type:"control"},
                        { id:"tbShowLabels",
                          html:"<p><input type='checkbox' id='tbShowLabels' checked><strong> Show labels</strong></p>",
                          value: "checked",
                          refresh: function(){self.refreshMarks();}, 
                          type:"control"},
                        { id:"shLoad", 
                          html:"<div id='shLoad'></div>", 
                          type:"info"},
                        { id:"infoAppliedFilters",
                          html:"<div id='infoAppliedFilters></div>",
                          type:"info"},
                        { id:"miniTabCtl",
                          html:"<div id='miniTabCtl></div>",
                          type:"tab"
                        }
                      ]
           self.innerTabInterface =
                    [
                      {
                      id: "tabFilter",
                      label: "Filters",
                      onclick: function(){ self.activateInnerTab("tabFilter");},
                      ctls:[
                        { id:"shFilter", 
                          html:"<div id='shFilter'><h3>Filter</h3></div>", 
                          type:"head"},
                        { id:txtFieldCtl.idName,
                          html: "<p>" + buildHTMLdropdown(txtFieldCtl, txtFieldItems),
                          value: "borough_ref",
                          refresh: function(){  self.storeTabValues("tabFilter");},
                          type:"control"},
                        { id:"txtSearch",
                          html:"<strong> for: </strong><input type='text' id='txtSearch'></p>",
                          value: "",
                          refresh: function(){  self.storeTabValues("tabFilter");},
                          type:"control"}, 
                        { id:statCtl.idName, 
                          html:"<p>" + buildHTMLdropdown(statCtl, statItems) + "</p>", 
                          value:1, 
                          refresh: function(){  self.storeTabValues("tabFilter");},
                          type:"control"},
                        { id:borCtl.idName, 
                          html:"<p>" + buildHTMLdropdown(borCtl, self.borItems) + "</p>", 
                          value:"All Boroughs", 
                          refresh: function(){  self.storeTabValues("tabFilter");},
                          type:"control"},
                        { id:"datFrom", 
                          html:"<p><strong>From:</strong> <input type='date' id='datFrom' value='2006-01-01' min='2006-01-01' max='2020-12-31'>", 
                          value:"2006-01-01", 
                          refresh: function(){  self.storeTabValues("tabFilter");},
                          type:"control"},
                        { id:"datTo", 
                          html:" <strong>To:</strong> <input type='date' id='datTo' value='2019-12-31' min='2006-01-01' max='2019-12-31'></p>", 
                          value:"2019-12-31", 
                          refresh: function(){  self.storeTabValues("tabFilter");},
                          type:"control"},
                        { id:"shiSlider", 
                          html:"<p><div id='shiOutput'><strong>Minimum Housing Impact:</strong> 0</div>" +
                              "<input type='range' min='0' max='150' value='0' class='slider' id='shiSlider'></p>",
                          value:0,
                          refresh: function(){ self.storeTabValues("tabFilter");
                                               var shihtml = "<strong>Minimum Housing Impact:</strong> " + self.meta.doc.getElementById("shiSlider").value;
                                               self.meta.doc.getElementById("shiOutput").innerHTML = shihtml; 
                                             },
                          type:"control" },
                        { id: "applyBtn",
                          html: "<button type='button' id='applyBtn' disabled>Apply</button>",
                          disabledState: true,
                          onclick: function(){  self.refreshMarks();
                                                self.centreOnGeoUnit();
                                                self.setDisabledStateFilterAppBtn(true)},
                          type: "button"
                        }
                      ]
                    },
                    {
                      id: "tabSummary",
                      label: "Summary",
                      onclick: function(){  self.activateInnerTab("tabSummary");
                                            self.displaySummary();},
                      ctls: [
                        {id:"shSumm", html:"<div id='shSumm'><h3>Summary Data</h3></div>", type:"head"},
                        {id:"summaryData", html:"<div id='summaryData' class='innerTabScrollBox'></div>", type:"info"}
                      ]
                     },
                     {
                       id: "tabListing",
                       label: "List",
                       onclick: function(){ self.activateInnerTab("tabListing");
                                            self.displaySearchList();},
                       ctls: [
                        {id:"shList", html:"<div id='shList'><h3>Application List</h3></div>", type:"head"},
                        { id:sortByCtl.idName, 
                          html:"<p>" + buildHTMLdropdown(sortByCtl, sortByItems), 
                          value:"permission_date", 
                          refresh: function(){  self.storeTabValues("tabListing");
                                                self.displaySearchList();},
                          type:"control"},
                        { id:sortOrderCtl.idName, 
                          html:" " + buildHTMLdropdown(sortOrderCtl, sortOrderItems) + "</p>", 
                          value: "asc", 
                          refresh: function(){  self.storeTabValues("tabListing");
                                                self.displaySearchList();},
                          type:"control"},
                        {id:"searchList", html:"<div id='searchList' class='innerTabScrollBox'></div>", type:"info"}
                      ]
                     },
                     {     
                       id: "tabDetails",
                       label: "Details",
                       onclick: function(){ self.activateInnerTab("tabDetails");
                                            self.displayDetails(self.examinedApp, true)},
                       ctls: [    
                        {id:"shDetails", html:"<div id='shDetails'><h3>Details of the Application</h3></div>", type:"info"},
                        {id:"divOpenAppDetails", html:"<div id='divOpenAppDetails' class='innerTabScrollBox'>"},
                        {id:"coreAppDetails", html:"<span id='coreAppDetails'></span>", type:"info"},
                        {id:"shAppStageDetails", html:"<span id='shAppStageDetails'><h3>Application Stages</h3></span>", type:"info"},
                        { id:appStageCtl.idName, 
                          html:"<p>" + buildHTMLdropdown(appStageCtl, appStageItems) + "</p>", 
                          value:"0", 
                          refresh: function(){  self.storeTabValues("tabDetails");
                                                self.displayAppStage()},
                          type:"control",
                          ctlVals:appStageCtl},
                        {id:"appStageDetails", html:"<span id='appStageDetails'</span>", type:"info"},
                        {id:"spanCloseAppDetails", html:"<span id='spanCloseAppDetails></span></div>"}
                      ]  
                     }
                    ];

        self.ctlRef = {};
    }

    function getBoroughList(borZoom){
        bor = [
           {val:"All Boroughs", itemName:"All Boroughs", lat:51.5073219, lon:-0.1276474, zoom:11, housingImpact:{}},
           {val:"Barking and Dagenham", itemName:"Barking and Dagenham", lat:51.5544867, lon:0.1498537, zoom:borZoom, housingImpact:{}},
           {val:"Barnet", itemName:"Barnet", lat:51.6135636, lon:-0.2112689, zoom:borZoom, housingImpact:{}},
           {val:"Bexley", itemName:"Bexley", lat:51.4625359, lon:0.1424610, zoom:borZoom, housingImpact:{}},
           {val:"Brent", itemName:"Brent", lat:51.5637061, lon:-0.2768436, zoom:borZoom, housingImpact:{}},
           {val:"Bromley", itemName:"Bromley", lat:51.3674577, lon:0.0604315, zoom:borZoom, housingImpact:{}},
           {val:"Camden", itemName:"Camden", lat:51.5431852, lon:-0.1634566, zoom:borZoom, housingImpact:{}},
           {val:"Croydon", itemName:"Croydon", lat:51.3557039, lon:-0.0625286, zoom:borZoom, housingImpact:{}},
           {val:"Ealing", itemName:"Ealing",  lat:51.5252635, lon:-0.3140294, zoom:borZoom, housingImpact:{}},
           {val:"Enfield", itemName:"Enfield", lat:51.6484796, lon:-0.0815612, zoom:borZoom, housingImpact:{}},
           {val:"Greenwich", itemName:"Greenwich", lat:51.4677524, lon:0.0472697, zoom:borZoom, housingImpact:{}},
           {val:"Hackney", itemName:"Hackney", lat:51.5493291, lon:-0.0480898, zoom:borZoom, housingImpact:{}},
           {val:"Hammersmith and Fulham", itemName:"Hammersmith and Fulham", lat:51.4981468, lon:-0.2284865, zoom:borZoom, housingImpact:{}},
           {val:"Haringey", itemName:"Haringey", lat:51.5877242, lon:-0.1054813, zoom:borZoom, housingImpact:{}},
           {val:"Harrow", itemName:"Harrow", lat: 51.5979317, lon:-0.3370282, zoom:borZoom, housingImpact:{}},
           {val:"Havering", itemName:"Havering", lat:51.5583163, lon:0.2491486, zoom:borZoom, housingImpact:{}},
           {val:"Hillingdon", itemName:"Hillingdon", lat:51.5430928, lon:-0.4485393, zoom:borZoom, housingImpact:{}},
           {val:"Hounslow", itemName:"Hounslow", lat:51.4620518, lon:-0.3802763, zoom:borZoom, housingImpact:{}},
           {val:"Islington", itemName:"Islington", lat:51.5475893, lon:-0.0995679, zoom:borZoom, housingImpact:{}},
           {val:"Kensington and Chelsea", itemName:"Kensington and Chelsea", lat:51.5041000, lon:-0.2008312, zoom:borZoom, housingImpact:{}},
           {val:"Kingston upon Thames", itemName:"Kingston upon Thames", lat:51.3817485, lon:-0.2762724, zoom:borZoom, housingImpact:{}},
           {val:"Lambeth", itemName:"Lambeth", lat:51.4601959, lon:-0.1224009, zoom:borZoom, housingImpact:{}},
           {val:"Lewisham", itemName:"Lewisham", lat:51.4524393, lon:-0.0152435, zoom:borZoom, housingImpact:{}},
           {val:"Merton", itemName:"Merton", lat:51.4120432, lon:-0.1856680, zoom:borZoom, housingImpact:{}},
           {val:"Newham", itemName:"Newham", lat:51.5296578, lon:0.0308328, zoom:borZoom, housingImpact:{}},
           {val:"Redbridge", itemName:"Redbridge", lat:51.5870039, lon:0.0692017, zoom:borZoom, housingImpact:{}},
           {val:"Richmond upon Thames", itemName:"Richmond upon Thames", lat:51.4413272, lon:-0.3078970, zoom:borZoom, housingImpact:{}},
           {val:"Southwark", itemName:"Southwark", lat:51.4658988, lon:-0.0691303, zoom:borZoom, housingImpact:{}},
           {val:"Sutton", itemName:"Sutton", lat:51.3571256, lon:-0.1735669, zoom:borZoom, housingImpact:{}},
           {val:"Tower Hamlets", itemName:"Tower Hamlets", lat:51.5147531, lon:-0.0351481, zoom:borZoom, housingImpact:{}},
           {val:"Waltham Forest", itemName:"Waltham Forest", lat:51.5983970, lon:-0.0171161, zoom:borZoom, housingImpact:{}},
           {val:"Wandsworth", itemName:"Wandsworth", lat:51.4526576, lon:-0.2000280, zoom:borZoom, housingImpact:{}},
           {val:"Westminster", itemName:"Westminster", lat:51.5150880, lon:-0.1593473, zoom:borZoom, housingImpact:{}}
        ];
        return bor;
    }

    function resetBorListImpact(crit){
        lowYear = Number(crit.dtlow.substring(0,4));
        highYear = Number(crit.dthigh.substring(0,4));
        self.summYearRange = [lowYear, highYear];
        self.borItems = getBoroughList(self.borZoom);
        for(var i=0; i<self.borItems.length; i++){
            for(var y=lowYear; y<=highYear; y++){
                ys = y.toString();
                self.borItems[i].housingImpact[ys] = 0;
            }
        }
    }

    function setupCats(meta){
        self.resCat = meta.resCat;
        self.resCat[null] = "Unclassified";
        self.spaceCat = meta.osCat;
        self.spaceCat[null] = "Unclassified";
        self.planCat = meta.puCat;
        self.planCat[null] = "Unclassified";
    }
        

    self.centreOnGeoUnit = function(){
        var val = self.ctlRef.borDropdown.options[self.ctlRef.borDropdown.selectedIndex].value;
        var lat = self.st.lat;
        var lon = self.st.lon;
        var z = self.st.zoom;
        for (var i=0; i<self.borItems.length; i++){
            if (self.borItems[i].val == val){
                lat = self.borItems[i].lat;
                lon = self.borItems[i].lon;
                z = self.borItems[i].zoom;
            }
        }
        self.meta.map.setView([lat,lon],z);
    }

    self.setVisible = function(bol){
       self.state.visible = bol;
         if(bol){
              self.refreshMarks();
          }else{
              self.removeMarks();
          }
      }

    self.getData = function(){
        var xmlhttp = new XMLHttpRequest();
        xmlhttp.onreadystatechange = function() {
            if (this.readyState == 4) {
                self.meta.doc.getElementById("systemMsg").innerHTML = "";
                initMarkers(this.responseText);
            }else if (this.readyState == 3){
                self.meta.doc.getElementById("systemMsg").innerHTML = " Loading data...";
            }else if (this.readyState == 2){
                self.meta.doc.getElementById("systemMsg").innerHTML = " Loading data..";
            }else if (this.readyState == 1){
                self.meta.doc.getElementById("systemMsg").innerHTML = " Loading data.";
            }
        };
        xmlhttp.open("POST", self.file, true);
        xmlhttp.send();
    }
    
    self.outputControls = function(){
        var rtn = "";
        for(var i=0; i<self.headerInterface.length; i++){
            rtn +=  self.headerInterface[i].html;
        }
        rtn += self.outputInnerTabInterface();
        return rtn;
    }

    self.outputInnerTabInterface = function(){
        var rtn = "";
        for(var i=0; i<self.innerTabInterface.length; i++){
            console.log("making button for : " + self.innerTabInterface[i].id);
            rtn += "<button class='tabLinks' id='" + self.innerTabInterface[i].id + "' disabled>" + self.innerTabInterface[i].label + "</button>";
        }
        rtn += "<div id='innerTabDisplay' class='innerTab'></div>";
        return rtn;
    }
  
    self.activateInnerTab = function(tabId){
        console.log("Activating inner control tabs");
        self.activeInnerTab = tabId;
        var tabHtml = self.outputActiveTab();
        self.meta.doc.getElementById(tabId).disabled = false;
        self.meta.doc.getElementById("innerTabDisplay").innerHTML = tabHtml;
        self.initInnerTab();
    }

    self.outputActiveTab = function(){
        var tabCtls = "";
        for(var i=0; i<self.innerTabInterface.length; i++){
            if (self.innerTabInterface[i].id == self.activeInnerTab){
                console.log("Correct inner tab found");
                for(var j=0; j<self.innerTabInterface[i].ctls.length; j++){
                    var ctlhtml = self.innerTabInterface[i].ctls[j].html;
                    console.log("html is : " + ctlhtml);
                    tabCtls += ctlhtml;
                }
            }else{
              console.log(self.innerTabInterface[i].id + " does not equal " + self.activeInnerTab + " at all");
            }
        }
        return tabCtls;
    }

    // Once we have outputted the controls, we need to be able to read them. Is there a better way? Almost certainly yes.
    self.initControls = function(doc){
        for (var i=0; i<self.headerInterface.length; i++){
            console.log(self.headerInterface[i].id);
            self.headerInterface[i].ctl = doc.getElementById(self.headerInterface[i].id);
            self.ctlRef[self.headerInterface[i].id] = self.headerInterface[i].ctl;
            //console.log(self.ctls[i].id + " added");
            if (self.headerInterface[i].type == "control"){
                self.headerInterface[i].ctl.value = self.headerInterface[i].value;
                self.headerInterface[i].ctl.oninput = self.headerInterface[i].refresh;
            }
        }
        for (var i=0; i<self.innerTabInterface.length; i++){
            var tabBtn = self.meta.doc.getElementById(self.innerTabInterface[i].id);
            tabBtn.onclick = self.innerTabInterface[i].onclick;
        }
    }

    self.initInnerTab = function(){
        for(var i=0; i<self.innerTabInterface.length; i++){
            if (self.innerTabInterface[i].id == self.activeInnerTab){
                console.log("Active inner tab readying for initiation");
                self.meta.doc.getElementById(self.innerTabInterface[i].id).innerHTML = "<strong>" + self.innerTabInterface[i].label + "</strong>";
                for(var j=0; j<self.innerTabInterface[i].ctls.length; j++){
                    self.innerTabInterface[i].ctls[j].ctl = self.meta.doc.getElementById(self.innerTabInterface[i].ctls[j].id);
                    console.log("Adding " + self.innerTabInterface[i].ctls[j].id)
                    self.ctlRef[self.innerTabInterface[i].ctls[j].id] = self.innerTabInterface[i].ctls[j].ctl;
                    //console.log(self.ctls[i].id + " added");
                    if (self.innerTabInterface[i].ctls[j].type == "control"){
                        self.innerTabInterface[i].ctls[j].ctl.value = self.innerTabInterface[i].ctls[j].value;
                        self.innerTabInterface[i].ctls[j].ctl.oninput = self.innerTabInterface[i].ctls[j].refresh;
                    }else if(self.innerTabInterface[i].ctls[j].type == "button"){
                        self.innerTabInterface[i].ctls[j].ctl.disabled = self.innerTabInterface[i].ctls[j].disabledState;
                        self.innerTabInterface[i].ctls[j].ctl.onclick = self.innerTabInterface[i].ctls[j].onclick;
                    }
                }
            }else{
                self.meta.doc.getElementById(self.innerTabInterface[i].id).innerHTML = self.innerTabInterface[i].label;
            }
        }
        if (self.activeInnerTab == "tabFilter"){ 
            var shihtml = "<strong>Minimum Housing Impact:</strong> " + self.meta.doc.getElementById("shiSlider").value; 
            self.meta.doc.getElementById("shiOutput").innerHTML = shihtml;
        }
    }

    function buildHTMLdropdown(ctl, arr){
        rtn = "\t<strong>" + ctl.label + ": </strong>\n";
        rtn += "\t<select class='" + ctl.className + "' name='" + ctl.idName + "' id = '" + ctl.idName + "'>";
        for(var i=0; i<arr.length; i++){
            rtn += "\t\t<option value='" + arr[i].val + "'>" + arr[i].itemName + "</option>\n";
        }        
        rtn += "</select>";
        return rtn;
    }

    function initMarkers(rawdata){
        self.data = JSON.parse(rawdata);
        self.state.ready = true;
        self.loadMarks();
    }
    
    function findBoroughItem(strName){
        var found = -1;
        for (var i=0; i<self.borItems.length; i++){
            if (self.borItems[i].val == strName){
                found = i;
            }
        } 
        return found;
    }

    self.loadMarks = function(){
        //console.log("adding map marks");
        crit = self.getCriteria();
        self.searchSet = [];
        resetBorListImpact(crit);
        self.totalimpact = 0;
        var meta = self.data.planApps.meta;
        setupCats(meta);
        self.data.planApps.planApp.sort(compareValues("permission_date"));
        for(var i=0; i<self.data.planApps.planApp.length; i++){
            var app = self.data.planApps.planApp[i];
            if(checkIfIncluded(app, crit)){
                self.totalimpact += parseInt(app["housingImpact"]);
                permYear = app.permission_date.substring(0,4);
                borNo = findBoroughItem(app.borough_name);
                if (borNo >= 0){
                    console.log("Adding " + app["housingImpact"].toString() + " to " + app.borough_name + permYear);
                    self.borItems[borNo].housingImpact[permYear] += Number(app["housingImpact"]);
                }
                var appMark;
                appr = self.setMapMarkAppearance(app, crit);
                if (app.geoPoly != null){
                    appMark = self.renderAppMarkAsPoly(appr, app);
                }else{
                    appMark = self.renderAppMarkAsCircle(appr, app);
                }
                appMark.addTo(self.meta.map);
                var se = self.makeSearchEntry(app);
                self.searchSet.push(se);
                self.mark.push(appMark);
            }
        }
        self.meta.doc.getElementById("tabSummary").disabled = false;
        self.meta.doc.getElementById("tabListing").disabled = false;
    }

    self.makeSearchEntry = function (app){
       var div = "<div class='seElement'>";
       var appAdd = app.prim_add_obj_name + ", " + app.street + ". " + app.post_code;
       var sehtml = div + appAdd + "</div>" +
                    div + "<strong>Permission date</strong>:" + app.permission_date + "</div>" + 
                    div + "<strong>Status</strong>:" + app.status_rc + "</div>" +
                    div + "<strong>Borough</strong>:" + app.borough_name + "</div>" +
                    div + "<strong>Impact</strong>:" + app.housingImpact + "</div>"; 
       var btnv = {  id:"btnView-" + app.permission_id,
                    html:"<button type='button' class='seBtn' id='btnView-" + app.permission_id 
                          + "'>view</button>",
                    input: function(){self.gotoEntryOnMap(app, true)}
                  };
       var btng = {  id:"btnGoto-" + app.permission_id,
                    html:"<button type='button' class='seBtn' id='btnGoto-" + app.permission_id  
                         + "'>goto</button>",
                    input: function(){
                                      if (self.examinedApp != null){
                                        oldSelEntry = self.meta.doc.getElementById("se" + self.examinedApp.permission_id);
                                        oldSelEntry.classList.add("seDefault");
                                        oldSelEntry.classList.remove("seSelected");
                                      }
                                      self.gotoEntryOnMap(app, false)
                                      newSelEntry = self.meta.doc.getElementById("se" + app.permission_id);
                                      newSelEntry.classList.add("seSelected");
                                      newSelEntry.classList.remove("seDefault");
                                    }
                  };
/*       console.log(btng.id);
       console.log(btng.html);
       console.log(btnv.id);
       console.log(btnv.html);*/
       var se = {  "lat": app.lat,
                   "lon": app.lon,
                   "planapp": app,
                   "html": sehtml,
                   "btnGoto": btng,
                   "btnView": btnv};
       return se;
    }

    self.renderAppMarkAsCircle = function(appr, app){
        var labeltext = "<span style='color:" + appr.tc + ";'>" + appr.pn + "</span>"
        var appMark = L.circle([app.lat, app.lon], {
            color: appr.lc,
            opacity: appr.op,
            fillColor: appr.fc,
            fillOpacity: appr.op,
            radius: appr.rd,
            planapp: app,
            weight: appr.wt
        });
        appMark.on("click", function(e){
            self.meta.tabs.openTabById(self.id);
            self.activateInnerTab("tabDetails");
            self.examinedApp = e.target.options.planapp;
            self.displayDetails(e.target.options.planapp, true);
        });
        appMark.bindTooltip(labeltext, {permanent: true,  direction: "center", offset: [0, 0], opacity: 1, className: "mapLabel" });
        return appMark;
    }

    self.renderAppMarkAsPoly = function(appr, app){
        //console.log("Adding poly of site");
        var labeltext = "<span style='color:" + appr.tc + ";'>" + appr.pn + "</span>"
        var poly = app.geoPoly.point;
        var ll = [];
        for (var i=0; i< poly.length; i++){
            ll.push([poly[i].lat, poly[i].lon]);
        }
        var appMark = L.polygon(ll, {
            color: appr.lc,
            opacity: appr.op,
            fillColor: appr.fc,
            fillOpacity: appr.filop,
            planapp: app,
            weight: appr.wt
        }); 
        appMark.on("click", function(e){
            self.meta.tabs.openTabById(self.id);
            self.activateInnerTab("tabDetails");
            self.examinedApp = e.target.options.planapp;
            self.displayDetails(e.target.options.planapp, true);
        });
        appMark.bindTooltip(labeltext, {permanent: true,  direction: "center", offset: [0, 0], opacity: 1, className: "mapLabel" });
        return appMark;
    }

    self.storeTabValues = function(tabId){
        for (var i=0; i<self.innerTabInterface.length; i++){
            if (self.innerTabInterface[i].id == tabId){
                for (var j=0; j<self.innerTabInterface[i].ctls.length; j++){
                    if (self.innerTabInterface[i].ctls[j].type == "control"){
                        self.innerTabInterface[i].ctls[j].value = self.innerTabInterface[i].ctls[j].ctl.value;
                    }else if(self.innerTabInterface[i].ctls[j].id == "applyBtn"){
                        self.innerTabInterface[i].ctls[j].disabledState = false;
                    }
                }
             }
          }
        self.ctlRef["applyBtn"].disabled = false;
    }
  
    self.setDisabledStateFilterAppBtn = function(bol){
        for (var i=0; i<self.innerTabInterface.length; i++){
            if (self.innerTabInterface[i].id == "tabFilter"){
                for (var j=0; j<self.innerTabInterface[i].ctls.length; j++){
                    if(self.innerTabInterface[i].ctls[j].id == "applyBtn"){
                        self.innerTabInterface[i].ctls[j].disabledState = bol;
                    }
                }
            } 
        }
        self.ctlRef["applyBtn"].disabled = bol;
    }

    self.refreshMarks = function(){
        for (var i=0; i<self.headerInterface.length; i++){
            if(self.headerInterface[i].type == "control"){
                console.log(self.headerInterface[i].id + ":" + self.headerInterface[i].ctl.value);
                self.headerInterface[i].value = self.headerInterface[i].ctl.value;
            }
//            self.ctls[i].value  
        }
        if(self.state.ready && self.state.visible){
          self.removeMarks();
          self.loadMarks();
        }
    }

    self.removeMarks = function(){
      //console.log("removing map marks");
      for(var i=0; i<self.mark.length; i++){
          if (self.mark[i]) { // check
              self.meta.map.removeLayer(self.mark[i]); // remove
          }
      }
      self.mark = [];
    }

    self.setMapMarkAppearance = function (pt, crit){
        var ck = crit["color"];
        var textColor = self.colors[ck].text;
        var lineColor = self.colors[ck].detail;
        var fillColor = self.colors[ck].base;
        var weight = self.colors[ck].weight;
        var ve = "";
        var opac = 0.6;
        var fillopac = 0.4;
        var rad = 12; 
        if(checkIfIncluded(pt, crit)){
             var impact = pt[crit.legend]
             if (crit.legend == "housingPcShift" && crit.showlabels){
                  ve = Math.round(impact * 100).toString() + "%";
             }else if (crit.showlabels){
                  ve = Math.round(impact).toString();
             }
             if (impact < 0){ 
                  lineColor = 'red';
                  textColor = 'red';
             }else if (impact == 0){
                  lineColor = fillColor;
                  textColor = self.colors[ck].detail;
             }else if (crit.showlabels){
                  ve = "+" + ve;
             } 
             if (pt.status_rc == 'COMPLETED'){
                  opac = 0.75;
                  fillopac = 0.6;
             }   
             var shig = Math.sqrt(pt[crit.legend] * pt[crit.legend]);
             if (shig - crit.shi > 100){
                  rad = 50; 
             }else if (shig - crit.shi > 20){
                  rad = 25; 
             }
        }
        mma = {tc: textColor, lc:lineColor, fc: fillColor, op: opac, filop: fillopac, rd: rad, pn: ve, wt: weight};
        return mma;
    }

    function compareValues(key, order = 'asc') {
      return function innerSort(a, b) {
        if (!a.hasOwnProperty(key) || !b.hasOwnProperty(key)) {
          // property doesn't exist on either object
          return 0;
        }

        const varA = (typeof a[key] === 'string')
          ? a[key].toUpperCase() : a[key];
        const varB = (typeof b[key] === 'string')
          ? b[key].toUpperCase() : b[key];

        let comparison = 0;
        if (varA > varB) {
          comparison = 1;
        } else if (varA < varB) {
          comparison = -1;
        }
        return (
          (order === 'desc') ? (comparison * -1) : comparison
        );
      };
    }

    function checkIfIncluded(pt,crit){
        var rtn = true;
//      var year = parseInt(pt.permission_date.substring(0,4),10)
        var lookAtTxt = false;
        var txtCheck = pt[crit.txtfield];
        if (txtCheck.length > 0 && crit.txtsearch.length > 0){
          console.log("Checking text field for " + pt.permission_id);
          txtCheck = txtCheck.match(/\w/g).join("").toUpperCase();
          crit.txtsearch = crit.txtsearch.match(/\w/g).join("").toUpperCase();
          lookAtTxt = true;
        }

        if (crit.dtlow > pt.permission_date || crit.dthigh < pt.permission_date){
            rtn = false;
        }else if  (crit.borough != "All Boroughs" && crit.borough != pt.borough_name){
            rtn = false;
        }else if ((crit.stat == "3" && pt.status_rc != "COMPLETED") || (crit.stat == "2" && pt.status_rc != "SUBMITTED" && pt.status_rc != "STARTED") || (pt.status_rc == 'LAPSED') || (pt.status_rc == 'SUPERSEDED')){
            rtn = false;
        }else if (Math.sqrt(pt.housingImpact * pt.housingImpact) < crit.shi){
            rtn = false;
        }else if (lookAtTxt){
            if (txtCheck.includes(crit.txtsearch) == false){
                rtn = false;
            }
        }else{
            //console.log(txtCheck);
            //console.log(crit.txtsearch);
            //console.log(crit.txtfield);
        }
        return rtn;
    }

     
    self.getCriteria = function(){
        var crit = {
          showlabels: self.ctlRef["tbShowLabels"].checked,
          legend: self.ctlRef["legDropdown"].options[self.ctlRef.legDropdown.selectedIndex].value,
          stat: self.ctlRef["statusDropdown"].options[self.ctlRef.statusDropdown.selectedIndex].value,
          txtfield: self.ctlRef["txtFieldDropdown"].options[self.ctlRef.txtFieldDropdown.selectedIndex].value,
          txtsearch: self.ctlRef["txtSearch"].value,
          stat: self.ctlRef["statusDropdown"].options[self.ctlRef.statusDropdown.selectedIndex].value,
          borough: self.ctlRef["borDropdown"].options[self.ctlRef.borDropdown.selectedIndex].value,
          shi: self.ctlRef["shiSlider"].value,
          dtlow: self.ctlRef["datFrom"].value,
          dthigh: self.ctlRef["datTo"].value,
          color: self.ctlRef["colDropdown"].options[self.ctlRef.colDropdown.selectedIndex].value
        };
        return crit
    }
      
    self.displaySummary = function(){
        self.meta.tabs.openTabById(self.id);
        var summtext = "<p><strong>Housing impact for designated type:</strong> " + self.totalimpact.toString() + "</p>\n";
        summtext += "<table><tr><td></td>";
        var ttlYear = {};
        for (var y = self.summYearRange[0]; y <= self.summYearRange[1]; y++){
            summtext+= "<td>" + y.toString() + "</td>";
            ttlYear[y.toString()] = 0;
        }
        summtext+= "<td>Total</td></tr>\n";
        for (var i=1; i < self.borItems.length; i++){ //i starts at 1 to miss 'All Boroughs' item
            summtext += "<tr><td>" + self.borItems[i].itemName + "</td>";
            var b = self.borItems[i].val;
            var ttlBor = 0;
            for (var y = self.summYearRange[0]; y <= self.summYearRange[1]; y++){
                impact = self.borItems[i].housingImpact[y.toString()];
                console.log("Impact at " + y.toString() + " in " + self.borItems[i].itemName + " is " + impact.toString());
                summtext += "<td>" + impact.toString() + "</td>";
                ttlBor += impact;
                ttlYear[y.toString()] += impact;
            }
            summtext += "<td>" + ttlBor.toString() + "</td></tr>\n";
        }
        summtext += "<tr><td>Total</td>";
        for (var y = self.summYearRange[0]; y <= self.summYearRange[1]; y++){
            summtext+= "<td>" + ttlYear[y.toString()].toString() + "</td>";
        }
        summtext += "</tr></table>\n";
        self.ctlRef.summaryData.innerHTML = summtext;
    }

    self.displaySearchList = function(){
        self.meta.tabs.openTabById(self.id);
        var sortBy = self.ctlRef["sortByDropdown"].options[self.ctlRef.sortByDropdown.selectedIndex].value;
        console.log("Sorting by:" + sortBy);
        var sortOrder = self.ctlRef["sortOrderDropdown"].options[self.ctlRef.sortOrderDropdown.selectedIndex].value;
        var searchtext = "";
        //self.searchSet.sort(function(a, b){return a.planapp[sortBy]});
        if (sortBy == "housingImpact"){
          self.searchSet.sort(function(a, b){return a.planapp[sortBy]-b.planapp[sortBy]});
        }else{
          self.searchSet.sort((a, b) => (a.planapp[sortBy] > b.planapp[sortBy]) ? 1 : -1);
        }
        if (sortOrder == "desc"){
            self.searchSet.reverse();
        }
        if (self.examinedApp == null){
          for (var i=0; i<self.searchSet.length; i++){
              searchtext += "<searchEntry class='seDefault' id='se" + self.searchSet[i].planapp.permission_id + "'>" + self.searchSet[i].btnView.html + self.searchSet[i].btnGoto.html 
                            + self.searchSet[i].html + "</searchEntry>\n";
          }
        }else{ 
          for (var i=0; i<self.searchSet.length; i++){
              cssClass = "seDefault";
              if (self.searchSet[i].planapp.permission_id == self.examinedApp.permission_id){
                console.log("one examined app found");
                cssClass = "seSelected";
              }
              searchtext += "<searchEntry class='" + cssClass + "' id='se" + self.searchSet[i].planapp.permission_id + 
                "'>" + self.searchSet[i].btnView.html + self.searchSet[i].btnGoto.html + self.searchSet[i].html + "</searchEntry>\n";
          }
        } 
        self.ctlRef.searchList.innerHTML = searchtext;
        if (self.examinedApp != null){
          document.getElementById('se' + self.examinedApp.permission_id).scrollIntoView();
        }
        self.initSearchButtons();
    }

    self.initSearchButtons = function(){
        for (var i=0; i<self.searchSet.length; i++){
            self.searchSet[i].btnView.ctl = self.meta.doc.getElementById(self.searchSet[i].btnView.id);
            self.searchSet[i].btnGoto.ctl = self.meta.doc.getElementById(self.searchSet[i].btnGoto.id);
            self.searchSet[i].btnView.ctl.onclick = self.searchSet[i].btnView.input;
            self.searchSet[i].btnGoto.ctl.onclick = self.searchSet[i].btnGoto.input;
        }
    
    }

      self.gotoEntryOnMap = function(pt, bolOpen){
           var z = 15; 
           self.meta.map.setView([pt.lat,pt.lon],z);
           self.examinedApp = pt;
           self.displayDetails(pt, bolOpen);
      }

      self.displayDetails = function(pt, bolOpen){
          self.meta.pointer.refocus(pt.lat, pt.lon);
          self.pt = pt;
          if (bolOpen){
              self.meta.tabs.openTabById(self.id);
              self.activateInnerTab("tabDetails");
          }
          text = "<p><strong>Borough Ref:</strong> " + pt.borough_ref + "</p>";
          text += "<p><strong>Address:</strong> " + pt.prim_add_obj_name + " " +  pt.street + ", " + pt.post_code + "</p>";
          text += "<p><strong>Borough:</strong> " + pt.borough_name + "</p>";
          text += "<p><strong>Status: </strong> " + pt.status_rc + "</p>";
          text += "<p><strong>Permission granted on:</strong> " + pt.permission_date + "</p>";
          if (pt.status_rc == "COMPLETED"){
              text += "<p><strong>Completed on: </strong> " + pt.completed_date + "</p>";
          } else {
              text += "<p><strong>Permission lapses on: </strong>" + pt.permission_lapses_date + "</p>";
          }
          text += "<p><strong>Description:</strong> " + pt.descr + "</p>";
          self.ctlRef.coreAppDetails.innerHTML = text;
          self.ctlRef.shAppStageDetails = "<h3>Application Stages</h3>";
          var opt = [];
          var as =  pt.appStages.appStage;
          var appstagedd = self.ctlRef.appStageDropdown;
          appstagedd.options[0] = null;
          if (Array.isArray(as)){
              for (var i=0; i < as.length; i++){
//                  opt.push({value: i.toString(), text: as[i].permission_date + " - " + as[i].borough_ref});
                  var option = self.meta.doc.createElement('option');
                  option.text = as[i].permission_date + " - " + as[i].borough_ref
                  option.value = i.toString();
                  appstagedd.add(option, i);
              }
          }else{
              var option = self.meta.doc.createElement('option');
              option.text = as.permission_date + " - " + as.borough_ref
              option.value = "0";
              appstagedd.add(option, 0);
          }
          self.displayAppStage();
      }

      self.deleteThis = function(){
          self.removeMarks();
          self = null;
      }

      self.displayAppStage = function(){
          var text = ""
          var table = [  {id: "housingTable", title: "Housing Unit", exist: "existingHousing", prop:"proposedHousing", type: "housingtype", cat: self.resCat, unit: "units"},
                          {id: "openSpaceTable", title: "Hectares of Space", exist: "existingOpenSpace", prop:"proposedOpenSpace", type: "spacetype", cat: self.spaceCat, unit: "hectares"},
                          {id: "floorspaceTable", title: "Non-residential Floorspace", exist: "existNonResFloorspace", prop:"propNonResFloorspace", type: "planninguseclass", cat: self.planCat, unit: "floorspace"},
                          {id: "nonResAccomTable", title: "Other Accommodation", exist: "existNonResAccom", prop:"propNonResAccom", type: "planninguseclass", cat: self.planCat, unit: "accom"}
                       ]
          asn = Number(self.ctlRef.appStageDropdown.options[self.ctlRef.appStageDropdown.selectedIndex].value);
          for (var i=0; i<table.length; i++){
              text += makeTable(self.pt, asn, table[i]);
          }
          self.ctlRef.appStageDetails.innerHTML = text;
      }

      function makeTable(pt, asn, fld){
          var erl = getExistingLines(pt, fld["exist"]);
          var existH = getAppLines(erl, fld);
          appStages = pt.appStages.appStage;
          var prl = getLinesFromAppStages(appStages, asn, fld["prop"]); 
          var propH = getAppLines(prl, fld);
          var ht = "";
          if (!!prl || !!erl){
              var hl = createChangeList(existH, propH, fld["cat"]);
              var ht = makeTableHTML(hl, fld["title"], fld["id"]);
          }
          return ht;
      }

      function getExistingLines(pt, type){
          var erl;
          //console.log("looking for " + type);
          if (pt[type] == null){
              erl = null;
          } else {
              erl = pt[type]["line"];
              console.log(type + "detected")
          }
          return erl;
      }

      function getLinesFromAppStages(appStages, l, type){
          var prl;
          //console.log("Looking for " + type);
          if (Array.isArray(appStages)){
          //    l = appStages.length - 1;
              if (appStages[l][type] == null){
                  prl = null;
              }else{
                  prl = appStages[l][type]["line"];
              }
          } else {
              if (appStages[type] == null){
                  prl = null; 
              }else{
                  prl = appStages[type]["line"];
              }
          }
          return prl;
      }

     function getAppLines(rl, fld){
          var rh = {};
          var type = fld.type;
          var unit = fld.unit;
          if (Array.isArray(rl)){
              for(i=0; i< rl.length; i++){
                  rh[rl[i][type]] = rl[i][unit];
              }
          }else if (!!rl){
              rh[rl[type]] = rl[unit];
          }
          return rh; 
      }

      function createChangeList(erh, prh, catset){
          var krc = Object.keys(catset);
          var hl = [];
          //console.log("Creating " + type + " table");
          for (var i=0; i<krc.length; i++){
              //console.log("calculating " + type + " line");
              var ev = 0;
              var pv = 0;
              var val = false;
              if (!!erh[krc[i]]){
                  ev = erh[krc[i]];
                  //console.log("Adding existing : " + ev);
                  val = true;
              }
              if (!!prh[krc[i]]){
                  pv = prh[krc[i]];
                  //console.log("Adding proposed : " + ev);
                  val = true;
              }
              if (val){ 
                  var l;
                  l = {
                      htype: catset[krc[i]],
                      exist: parseFloat(ev), 
                      prop: parseFloat(pv),
                      net: pv - ev
                  }
                  hl.push(l);
                  //console.log("type: " + l.htype + "; existing: " + l.exist + "; proposed: " + l.prop + "; net: " + l.net); 
              }
          }
          var ttl = { htype: "Total", exist: 0, prop: 0, net: 0 };
          for (var i=0; i<hl.length; i++){
              //console.log("Datatype...");
              //console.log(typeof hl[i].exist);
              ttl.exist += parseFloat(hl[i].exist);
              ttl.prop += parseFloat(hl[i].prop);
              ttl.net += parseFloat(hl[i].net);
          }
          hl.push(ttl);
          return hl;
     }    

   
      function makeTableHTML(hl, title, id){
          //console.log("Making table");
          if (id == "openSpaceTable"){ 
              rnd = 3;
              console.log("Open space table processed");
          }else{
              rnd = 0;
              console.log("Other table processed");
          }
          hTable = "<strong>" + title + "</strong>\n<table id='" + id + "'>\n<tr>\n\t<th>Type</th>\n\t<th>Existing</th>\n\t<th>%</th>\n\t<th>Proposed</th>\n\t<th>%</th>\n\t<th>Change</th>\n</tr>\n";
          for (i=0; i<hl.length; i++){
              existPc = Math.round((hl[i].exist / hl[hl.length-1].exist) *100);
              propPc = Math.round((hl[i].prop / hl[hl.length-1].prop) *100);
              hTable += "<tr>\n\t<td>" + hl[i].htype + "</td>\n";
              hTable += "\t<td>" + hl[i].exist.toFixed(rnd) + "</td>\n";
              hTable += "\t<td>" + existPc + "%</td>\n";
              hTable += "\t<td>" + hl[i].prop.toFixed(rnd) + "</td>\n";
              hTable += "\t<td>" + propPc + "%</td>\n";
              var pm = "";
              if (hl[i].net > 0){
                  pm = "+";
              }
              hTable += "\t<td>" + pm + hl[i].net.toFixed(rnd) + "</td>\n</tr>\n";
          }
          hTable +="</table>\n";
          return hTable;
      }
   
  }