Thursday, December 8, 2016

Integrating OL3 with geostats.js to produce Thematic Maps for Socio-Economic Data

Lately, I had a requirement building a "thematic mapping" application using openlayers 3 in order to visualise socio-economic data. OL3 does not provide any functionality related to statistical mapping. So googling it, I fell into geostats .
A tiny standalone JS library for classification. It is like a hidden treasure, which I never had the chance to test it. Within the project page there are a few examples related to OL2 but nothing related to OL3. So I though it would be nice to share my experience and give a boost to all GIS geeks looking forward to use the library in conjunction with OL3.

It currently supports the following statistical methods
  • equal intervals
  • quantiles
  • standard deviation (seems to have a small bug here)
  • arithmetic progression
  • geometric progression
  • jenks (natural breaks)
  • uniques values
  • user defined classification
So lets go for an example. Lets build a thematic population map for  world countries.

If you are too impatient ...... get a snapshot

But lets examine our code step by step:
Here is your html:
Nothing special. A selector for the method selected, a number spinner to get the number classes and two divs. One for the map and one to place the generated legend. And of course a button to draw our thematic map.

<select id="methodselector" class="form-control">
  <option value="method_EI" >Equal Interval</option>
  <option value="method_Q" selected>Quantile</option>
  <option value="method_SD" >Standard Deviation</option>
  <option value="method_AP" >Arithmetic Progression</option>
  <option value="method_GP" >Geometric Progression</option>
  <option value="method_CJ">Class Jenks</option>                  
</select>
Number of Classes:
<input type="number" id="classcount" min="1" value="5" max="10">
<input type="button" id="drawitbtn"  value='draw themmatic'/>

<div id="map" class="map"></div>

<div id='legend'></div>

Dont forget to reference the necessary js, css libs within your html file. These are::
<link rel="stylesheet" type="text/css" href="https://openlayers.org/en/v3.19.1/css/ol.css" />
<script src="https://openlayers.org/en/v3.19.1/build/ol-debug.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chroma-js/1.2.1/chroma.min.js"></script>
<script src="https://www.intermezzo-coop.eu/mapping/geostats/lib/geostats.js"></script>
:
Here is your css:
Again nothing special. Just a few classes to support the legend.


.geostats-legend div {
  margin: 3px 10px 5px 10px
}

.geostats-legend-title {
  font-weight: bold;
  margin-bottom: 4px;
}

.geostats-legend-block {
  border: 1px solid #555555;
  display: block;
  float: left;
  height: 12px;
  margin: 0 5px 0 20px;
  width: 20px;
}

.geostats-legend-counter {
  font-size: 0.8em;
  color: #666;
  font-style: italic;
}

And finally your JAVASCRIPT:
just go through the comments of code

$('#drawitbtn').click(drawIt);

//some global vars here
var classSeries;
var classColors;
//color start from
var colorFrom ='FFFFFF';
//color end to
var colorTo = '1A8B16';
var defaultStyle = new ol.style.Style({
  fill: new ol.style.Fill({
    color: 'rgba(255, 255, 255, 0.3)'
  }),
  stroke: new ol.style.Stroke({
    color: 'rgba(0, 255, 0, 1)',
    width: 1
  }),
  text: new ol.style.Text({
    font: '12px Calibri,sans-serif',
    fill: new ol.style.Fill({
      color: '#000'
    }),
    stroke: new ol.style.Stroke({
      color: '#fff',
      width: 3
    })
  })
});


//our methods here

var vectorLayer = new ol.layer.Vector({
  style:defaultStyle,
  source: new ol.source.Vector({
    url: 'https://gist.githubusercontent.com/ptsagkis/dacbe5a42856dee041294b54579095d4/raw/730879d0d4909622b273818235b9c7f510bab5ee/countries_siplified.geojson',
   format: new ol.format.GeoJSON({
              defaultDataProjection:'EPSG:4326',
              featureProjection:'EPSG:3857'
            })
  })
});

var map = new ol.Map({
  layers: [
    new ol.layer.Tile({
      source: new ol.source.OSM()
    }),
    vectorLayer
  ],
  target: 'map',
  view: new ol.View({
    center: [0, 0],
    zoom: 1
  })
});



/**
 * do the themmatic
 */
function drawIt(){
var countryPopVals = new Array();
vectorLayer.getSource().getFeatures().forEach(function(feat) {
countryPopVals.push(feat.get("POP2005"))
});
console.info("countryPopVals",countryPopVals);
getAndSetClassesFromData(countryPopVals, getClassNum(), getMethod());
vectorLayer.setStyle(setStyle);
}


/**
 * @data {Array} the array of numbers (these are the pop data for all countries)
 * @numclasses {Integer} get the number of classes
 * @method {String}  get the classification method
 * 
 *
 * set geostats object
 * set the series
 * set the colors ramp        
 * 
 */
function getAndSetClassesFromData(data, numclasses, method) {
  var serie = new geostats(data);
  var legenLabel = ""; 
  if (method === "method_EI") {
    serie.getClassEqInterval(numclasses);
    methodLabel = "Equal Interval";
  } else if (method === "method_Q") {
    serie.getClassQuantile(numclasses);
    methodLabel = "Quantile";
  } else if (method === "method_SD") {
    serie.getClassStdDeviation(numclasses);
    methodLabel = "Standard Deviation ";
  } else if (method === "method_AP") {
    serie.getClassArithmeticProgression(numclasses);
    methodLabel = "Arithmetic Progression";
  } else if (method === "method_GP") {
    serie.getClassGeometricProgression(numclasses);
    methodLabel = "Geometric Progression ";
  } else if (method === "method_CJ") {
    serie.getClassJenks(numclasses);
    methodLabel = "Class Jenks";
  } else {
  alert("error: no such method.")
  }
 var colors_x = chroma.scale([colorFrom, colorTo]).colors(numclasses)

serie.setColors(colors_x);
document.getElementById('legend').innerHTML = serie.getHtmlLegend(null, "World Population</br> Method:"+methodLabel, 1);
classSeries = serie;
classColors = colors_x; 
}




/**
 * function to verify the style for the feature
 */
function setStyle(feat,res) {
  var currVal = parseFloat(feat.get("POP2005")); 
  var bounds = classSeries.bounds;
  var numRanges = new Array();
  for (var i = 0; i < bounds.length-1; i++) { 
  numRanges.push({
      min: parseFloat(bounds[i]),
      max: parseFloat(bounds[i+1])
    });  
  }  
  var classIndex = verifyClassFromVal(numRanges, currVal);
  var polyStyleConfig = {
    stroke: new ol.style.Stroke({
      color: 'rgba(255, 0, 0,0.3)',
      width: 1
    })
  };

  var textStyleConfig = {};
  var label = res < 5000 ? feat.get('NAME') : '';
  if (classIndex !== -1) {
    polyStyleConfig = {
      stroke: new ol.style.Stroke({
        color: 'rgba(0, 0, 255, 1.0)',
        width: 1
      }),
      fill: new ol.style.Stroke({
        color: hexToRgbA(classColors[classIndex],0.7)
      })
    };
    textStyleConfig = {
      text: new ol.style.Text({
        text: label,
        font: '12px Calibri,sans-serif',
        fill: new ol.style.Fill({
          color: "#000000"
        }),
        stroke: new ol.style.Stroke({
          color: "#FFFFFF",
          width: 2
        })
      }),
      geometry: function(feature) {
        var retPoint;
        if (feature.getGeometry().getType() === 'MultiPolygon') {
          retPoint = getMaxPoly(feature.getGeometry().getPolygons()).getInteriorPoint();
        } else if (feature.getGeometry().getType() === 'Polygon') {
          retPoint = feature.getGeometry().getInteriorPoint();
        }
        
        return retPoint;
      }
    }
  };

  var textStyle = new ol.style.Style(textStyleConfig);
  var style = new ol.style.Style(polyStyleConfig);
  return [style, textStyle];
}

//*************helper functions this point forward***************//
 
function verifyClassFromVal(rangevals, val) {
  var retIndex = -1;
  for (var i = 0; i < rangevals.length; i++) {
    if (val >= rangevals[i].min && val <= rangevals[i].max) {
      retIndex = i;
    }
  }
  return retIndex;
}

/**
 *   get the user selected method
 */
function getMethod(){
var elem = document.getElementById("methodselector");
var val = elem.options[elem.selectedIndex].value;
return val;
}

/**
 *   get the user selected number of classes
 */
function getClassNum(){
var elem = document.getElementById("classcount");
return parseInt(elem.value);
}


/**
 * convert hex to rgba 
 *
 */
function hexToRgbA(hex,opacity) {
  var c;
  if (/^#([A-Fa-f0-9]{3}){1,2}$/.test(hex)) {
    c = hex.substring(1).split('');
    if (c.length == 3) {
      c = [c[0], c[0], c[1], c[1], c[2], c[2]];
    }
    c = '0x' + c.join('');
    return 'rgba(' + [(c >> 16) & 255, (c >> 8) & 255, c & 255].join(',') + ','+opacity+')';
  }
  throw new Error('Bad Hex');
}
/**
 *    get the maximum polygon out of the supllied  array of polygon
 *    used for labeling the bigger one
 */
function getMaxPoly(polys) {
  var polyObj = [];
  //now need to find which one is the greater and so label only this
  for (var b = 0; b < polys.length; b++) {
    polyObj.push({
      poly: polys[b],
      area: polys[b].getArea()
    });
  }
  polyObj.sort(function(a, b) {
    return a.area - b.area
  });

  return polyObj[polyObj.length - 1].poly;
}


Enjoy all of you

Urban Growth Lab - UGLab

Proud to release a first version of the UGLab project .  UGLab is a python open source project capable to execute Urban Growth predictions f...