/**
* Global Sensor Networks (GSN) Source Code
* Copyright (c) 2006-2016, Ecole Polytechnique Federale de Lausanne (EPFL)
*
* This file is part of GSN.
*
* GSN 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.
*
* GSN 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 GSN. If not, see <http://www.gnu.org/licenses/>.
*
* File: src/ch/epfl/gsn/vsensor/GridRenderer.java
*
* @author Sofiane Sarni
* @author Julien Eberle
*
*/
package ch.epfl.gsn.vsensor;
import org.slf4j.LoggerFactory;
import ch.epfl.gsn.beans.DataTypes;
import ch.epfl.gsn.beans.StreamElement;
import ch.epfl.gsn.beans.VSensorConfig;
import ch.epfl.gsn.utils.geo.GridTools;
import ch.epfl.gsn.vsensor.AbstractVirtualSensor;
import ch.epfl.gsn.vsensor.GridRenderer;
import org.slf4j.Logger;
import javax.imageio.ImageIO;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Hashtable;
import java.util.TreeMap;
public class GridRenderer extends AbstractVirtualSensor {
private static final transient Logger logger = LoggerFactory.getLogger(GridRenderer.class);
private static final String CELL_PIXELS = "cellpixels";
private static final String MAP_OVERLAY = "mapoverlay";
private static final String MAX_V = "max_value";
private static final String MIN_V = "min_value";
private int map[];
private double min_v;
private double max_v;
private double minvalue;
private double maxvalue;
private double cellsize;
private double yllcorner;
private double xllcorner;
private int ncols;
private int nrows;
//to avoid querying all the time the same image from osm, we cache it.
private Hashtable<Double,BufferedImage> cache = new Hashtable<Double,BufferedImage>();
int cell_pixels = 20;
boolean map_overlay = false;
@Override
public boolean initialize() {
VSensorConfig vsensor = getVirtualSensorConfiguration();
TreeMap<String, String> params = vsensor.getMainClassInitialParams();
// Approximative number of pixels for one cell of the array
String cell_pixels_str = params.get(CELL_PIXELS);
if (cell_pixels_str != null) {
cell_pixels_str = cell_pixels_str.trim().toLowerCase();
try {
cell_pixels = Integer.parseInt(cell_pixels_str.trim());
} catch (NumberFormatException e) {
logger.warn("Parameter \"" + CELL_PIXELS + "\" has incorrect value in Virtual Sensor file. Assuming default value.");
}
}
//value to be assigned to the min of the color scale
String min_str = params.get(MIN_V);
if (min_str != null) {
min_str = min_str.trim().toLowerCase();
try {
min_v = Double.parseDouble(min_str.trim());
} catch (NumberFormatException e) {
logger.warn("Parameter \"" + MIN_V + "\" has incorrect value in Virtual Sensor file.");
}
}
//value to be assigned to the max of the color scale
String max_str = params.get(MAX_V);
if (max_str != null) {
max_str = max_str.trim().toLowerCase();
try {
max_v = Double.parseDouble(max_str.trim());
} catch (NumberFormatException e) {
logger.warn("Parameter \"" + MAX_V + "\" has incorrect value in Virtual Sensor file.");
}
}
//using or not the osm overlay
String map_overlay_str = params.get(MAP_OVERLAY);
if (map_overlay_str != null) {
map_overlay_str = map_overlay_str.trim().toLowerCase();
if(map_overlay_str.equals("no") || map_overlay_str.equals("false")){
map_overlay = false;
}else if(map_overlay_str.equals("yes") || map_overlay_str.equals("true")){
map_overlay = true;
} else {
logger.warn("Parameter \"" + MAP_OVERLAY + "\" has incorrect value in Virtual Sensor file. Assuming default value.");
}
}
initColorMap();
return true;
}
@Override
public void dispose() {
}
@Override
public void dataAvailable(String inputStreamName, StreamElement streamElement) {
ncols = (Integer) streamElement.getData("ncols");
nrows = (Integer) streamElement.getData("nrows");
xllcorner = (Double) streamElement.getData("xllcorner");
yllcorner = (Double) streamElement.getData("yllcorner");
cellsize = (Double) streamElement.getData("cellsize"); // must be in meters
long timestamp = streamElement.getTimeStamp();
Double values[][] = GridTools.deSerialize((byte[]) streamElement.getData("grid"));
byte b[] = createImageFromArray(values);
StreamElement se = new StreamElement(new String[]{"grid"},
new Byte[]{DataTypes.BINARY},
new Serializable[]{b},
timestamp);
dataProduced(se);
}
/**
* earth radius taken from
* http://www.uwgb.edu/dutchs/UsefulData/UTMFormulas.htm
* grayscale conversion from
* http://en.wikipedia.org/wiki/Grayscale
* openstreetmap zoom conversion taken from
* http://wiki.openstreetmap.org/wiki/Zoom_levels
* @param a
* @return
*/
private byte[] createImageFromArray(Double[][] a) {
BufferedImage back;
// if the min and max values are not given, we search for them in the array of data
if ((min_v == 0 && max_v == 0) || min_v >= max_v){
minvalue = a[0][0];
maxvalue = a[0][0];
for (int i = 0; i < a.length; i++)
for (int j = 0; j < a[0].length; j++) {
if (minvalue > a[i][j]) minvalue = a[i][j];
if (maxvalue < a[i][j]) maxvalue = a[i][j];
}
}else{
minvalue = min_v;
maxvalue = max_v;
}
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
if (map_overlay){
//openstreetmap needs the center of the tile (lat/lon) for querying the map...
double centerY = yllcorner + (360/(6356752.0*2*Math.PI) * cellsize * nrows)/2;
double centerX = xllcorner + (360/(6378137*2*Math.PI*Math.cos(Math.toRadians(centerY)))*cellsize * ncols)/2;
//... and the zoom level
int zoom = (int) Math.ceil(Math.log(cell_pixels*6378137*2*Math.PI*Math.cos(Math.toRadians(centerY))/cellsize)/Math.log(2)-8);
//then we adjust the number of pixel per cell to match the zoom level's pixel/meter and compute the whole image size
int width = (int) (cellsize * ncols / (6378137*2*Math.PI*Math.cos(Math.toRadians(centerY))/Math.pow(2, zoom+8)));
int height = (int) (cellsize * nrows / (6378137*2*Math.PI*Math.cos(Math.toRadians(centerY))/Math.pow(2, zoom+8)));
//the two images that will be blent later
BufferedImage osmap = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
back = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
//getting map from the cache or directly from osm staticmap
try {//the key should guarantee some kind of uniqueness to the map tile
if (cache.containsKey(centerX+centerY*180+zoom*180*360+width*180*360*20+height*180*360*20*1000)){
osmap = cache.get(centerX+centerY*180+zoom*180*360+width*180*360*20+height*180*360*20*1000);
}else{
//You need to use a Tile Map Service for generating the map images.
//you can get a list here: http://wiki.openstreetmap.org/wiki/TMS
//Please read their policy and consider setting up you own service if you use it a lot
//The grid renderer is caching the tiles and only makes a new request when one of the 5 parameters changes or the server restarts
osmap = ImageIO.read(new URL("please insert the url here?center="+centerY+","+centerX+"&zoom="+zoom+"&size="+width+"x"+height));
cache.put(centerX+centerY*180+zoom*180*360+width*180*360*20+height*180*360*20*1000, osmap);
}
} catch (MalformedURLException e1) {
logger.error(e1.getMessage(), e1);
} catch (IOException e1) {
logger.error(e1.getMessage(), e1);
}
//go through the picture pixel by pixel
for (int x=0;x<width;x++)
for (int y=0;y<height;y++){
//the indices in the grid corresponding to this pixel
int i = (int) (x/(width * 1.0 / ncols));
int j = (int) (y/(height * 1.0 / ncols));
// convert map to black/white
int val = osmap.getRGB(x, y);
int r = (val & 0x00ff0000) >> 16;
int g = (val & 0x0000ff00) >> 8;
int b = (val & 0x000000ff);
int bw = (int)(0.2126*r+0.7152*g+0.0722*b);
//and blend it with the color from the grid of values
int color = mapValue(a[j][i]);
int r2 = (color & 0x00ff0000) >> 16;
int g2 = (color & 0x0000ff00) >> 8;
int b2 = (color & 0x000000ff);
int rgb = (bw+r2)/2 * 256 * 256 + (bw+g2)/2 * 256 + (bw+b2)/2;
back.setRGB(x, y, rgb);
}
} else {
int Y = a.length;
int X = a[0].length;
back = new BufferedImage(X * cell_pixels, Y * cell_pixels, BufferedImage.TYPE_INT_RGB);
int bigPixel[] = new int[cell_pixels * cell_pixels];
for (int i = 0; i < X; ++i)
for (int j = 0; j < Y; ++j) {
int color = mapValue(a[j][i]);
for (int k = 0; k < cell_pixels * cell_pixels; k++)
bigPixel[k] = color;
back.setRGB(i * cell_pixels, j * cell_pixels, cell_pixels, cell_pixels, bigPixel, 0, cell_pixels);
}
}
// draw the gradient of the scale
int bigPixel[] = new int[15];
for (int x=0;x<back.getHeight();x++){
int color = map[255-(int)(x*255.0/back.getHeight())];
for (int k = 0; k < 15; k++)
bigPixel[k] = color;
back.setRGB(0, x, 15, 1, bigPixel, 0,15);
}
// draw the min and max of the scale
Graphics2D gp = back.createGraphics();
gp.setColor(Color.black);
gp.drawString(""+maxvalue, 3, 12);
gp.drawString(""+minvalue, 3, back.getHeight()-3);
try {
ImageIO.write(back, "png", outputStream);
} catch (IOException e) {
logger.warn(e.getMessage(), e);
}
return outputStream.toByteArray();
}
// green-yellow-red color map
public void initColorMap() {
map = new int[256];
int r, g, b;
for (int i = 0; i <= 255; i++) {
if (i < 128)
r = 255;
else
r = 255 - i * 2;
if (i < 128)
g = i * 2;//i;
else
g = 255;
b = 0;//255;
map[255-i] = r * 256 * 256 + g * 256 + b;
}
}
/**
* Get the value of the color from the map and returns black if the value is greater than the max or white if the value is smaller than the min
* @param value: rgb color
* @return
*/
public int mapValue(double value) {
if (value > maxvalue)
return 0; //black
if (value < minvalue)
return 255 * 256 * 256 + 255 * 256 + 255; //white
return map[(int) Math.round((value - minvalue) * 255 / (maxvalue - minvalue))];
}
}