Showing posts with label javascript. Show all posts
Showing posts with label javascript. Show all posts

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

Tuesday, September 22, 2015

Openlayers 3 - load start / load end events on layers

With the latest versions of OL3 we have the ability to listen for the 'loadstart' and 'loadend' events on layers exist on our map. In openlayers 2 that was quite clear.

With the latest versions of  openlayers 3, things start to be a bit more clear, though not as clear as it used to be on OL2.

So for TILE (ol.layer.Tile) layers our code snip should look like:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
//declare the layer
var osmLayer =  new ol.layer.Tile({
      source: new ol.source.OSM()
    });
//asign the listeners on the source of tile layer
osmLayer.getSource().on('tileloadstart', function(event) {
//replace with your custom action
document.getElementById('tilesloaderindicatorimg').src = 'css/images/tree_loading.gif';
     });

osmLayer.getSource().on('tileloadend', function(event) {
//replace with your custom action
document.getElementById('tilesloaderindicatorimg').src = 'css/images/ok.png';
     });
osmLayer.getSource().on('tileloaderror', function(event) {
//replace with your custom action        
document.getElementById('tilesloaderindicatorimg').src = 'css/images/no.png';
     });

For WMS layers the approach is a bit different:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//declare the layer
var wmsLayer =   new ol.layer.Image({
    source: new ol.source.ImageWMS({
      attributions: [new ol.Attribution({
        html: '&copy; ' +
            '<a href="http://www.geo.admin.ch/internet/geoportal/' +
            'en/home.html">' +
            'National parks / geo.admin.ch</a>'
      })],
      crossOrigin: 'anonymous',
      params: {'LAYERS': 'ch.bafu.schutzgebiete-paerke_nationaler_bedeutung'},
      serverType: 'mapserver',
      url: 'http://wms.geo.admin.ch/'
    })
  });

//and now asign the listeners on the source of it
 var lyrSource = wmsLayer.getSource();
      lyrSource.on('imageloadstart', function(event) {
      console.log('imageloadstart event',event);
      //replace with your custom action
      var elemId = event.target.params_.ELEMENTID;
      document.getElementById(elemId).src = 'css/images/tree_loading.gif'; 
      });

      lyrSource.on('imageloadend', function(event) {
       console.log('imageloadend event',event);
      //replace with your custom action
      var elemId = event.target.params_.ELEMENTID;
      document.getElementById(elemId).src = 'css/images/ok.png'; 
      });

      lyrSource.on('imageloaderror', function(event) {
       console.log('imageloaderror event',event);
      //replace with your custom action
      var elemId = event.target.params_.ELEMENTID;
      document.getElementById(elemId).src = 'css/images/no.png'; 
      });  

For WFS Vecotr layers things are even more complicated:

so I made a fiddle you may see here -->fiddle

fiddle preview -->

Friday, August 28, 2015

OL3 (v3.20.0 ), draw holes on polygons. (javascript)

As ol3 have not yet implemented the "draw hole" vector functionality, I did a bit of work to achive it. So, in the meantime, my approach may help you. After the last update it works using 3.20.0 latest openlayers3 version. Propably it works with older versions as well but havent tested.

Also not that I use jsts library in order to verify whether drawn hole vertex is inside a polygon. 

Here is a demo  here(jsfiddle) (updated on 16/12/2016)

preview




It only works for simple (not multi) polygons. But you may add as many holes you like in a single polygon. It also lucks the visual hole effect during drawing (fixed with the new update).
Also prevents the draw hole ouside the selected polygon. 
I ll try to work with these and if I get something better I ll let you know.

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