Files
2017-04-29 12:01:29 +08:00

507 lines
18 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Montecarlo simulation of bushfire risk</title>
<link rel="stylesheet" type="text/css" href="/css/bootstrap.css" />
<link rel="stylesheet" type="text/css" href="/css/style.css" />
<!-- JS -->
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-10 col-md-offset-1">
<div class="page-header">
<h1>Bushfire Simulator</h1>
<h3>A simulation on landsat 8 data from NASA</h3>
</div>
<div class="buttons-group">
<button id="start" type="button">Start</button>
<button id="stop" type="button">Stop</button>
<button id="next" type="button">next</button>
<button id="reset" type="button">reset</button>
<div><label>Hours: </label><span id="tick">0</span></div>
</div>
<div id="canvases">
<!-- Hiden canvases used to generate rasters for leaflet, can be used for debugging -->
<style media="screen">
.layer1 {z-index: 1;}
.layer2 {z-index: 2; opacity: 0.5}
.layer3 {z-index: 3}
.layer4 {z-index: 4}
.layer5 {z-index: 5}
.overlay {position:absolute}
.overlay { width: 50%; opacity:0 }
</style>
<canvas id="map" class="layer1 overlay"></canvas>
<canvas id="fuel" class="layer2 overlay"></canvas>
<canvas id="ash" class="layer3 overlay"></canvas>
<canvas id="fire" class="layer4 overlay"></canvas>
<canvas id="elevation" class="layer5 overlay"></canvas>
</div>
<div style="width:100%; height:500px" id="leaflet-map">
</div>
</div>
</div>
</div>
</body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.js"></script>
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.js"></script> -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.8.0/d3.js"></script>
<!-- mapping -->
<link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.7.2/leaflet.css" />
<script src="http://cdn.leafletjs.com/leaflet-0.7.2/leaflet.js"></script>
<link rel="stylesheet" href="https://api.mapbox.com/mapbox.js/v2.2.1/mapbox.css">
<script src="https://api.mapbox.com/mapbox.js/v2.2.1/mapbox.js"></script>
<!-- We need matrices with convolution and reshape methods -->
<script src="https://rawgit.com/waylonflinn/weblas/master/dist/weblas.js"></script>
<script src="https://rawgit.com/nicolaspanel/numjs/893016ec40e62eaaa126e1024dbe250aafb3014b/dist/numjs.js"></script>
<script type="text/javascript">
class Simulation {
constructor(canvas,data) {
this.canvas=canvas
this.H = canvas.height = data.fuel.shape[0]
this.W = canvas.width = data.fuel.shape[1]
this.clock = 0;
data.fuel = data.fuel.reshape([data.fuel.shape[0],data.fuel.shape[1],1])
data.fire = nj.zeros(data.fuel.shape)
data.ash = nj.zeros(data.fuel.shape)
data.elev = data.elev.reshape([data.elev.shape[0],data.elev.shape[1],1])
this.data = nj.concatenate([data.fire,data.fuel,data.ash,data.elev])
var coords = this.ignite()
this.display();
}
/* set an initial fire */
ignite(e){
// TODO make click to ignore work with zooming
var canvas = this.canvas
// initial point away from walls
// we will make a nice bright cross so it's visible
var x,y
if (e){
// get click location FIXME might not work on all browsers
y=_.round(e.layerX/this.canvas.offsetWidth*this.canvas.width)
x=_.round(e.layerY/this.canvas.offsetHeight*this.canvas.height)
} else {
x = _.round(d3.randomUniform(20, this.W-20)())
y = _.round(d3.randomUniform(20, this.H-20)())
}
console.log('fire',x,y)
this.data.set(x,y,0,1)
this.data.set(x-1,y-1,0,0.3)
this.data.set(x+1,y-1,0,0.3)
this.data.set(x-1,y+1,0,0.3)
this.data.set(x+1,y+1,0,0.3)
this.data.set(x,y,1,0.3)
this.display()
return [x,y]
}
/* advance model by one tick */
tick(){
// PARAMS TODO move them
var fuelMultipler = 5 // how many turns it burns for
var fireMultiplier = 1
var fireGrowth = 1.5
var transmissionChance = 0.3
// distance to diagonal tiles as a ratio to tile size np.sqrt(1**2+1**2)
var diagDist = 1.42
// tick the environment
this.clock++;
var t0 = new Date().getTime()
document.getElementById('tick').innerText=this.clock
if (this.clock>1000) return 0;
console.log('tick',this.clock)
//
// we are modifying the data in place, so freeze a copy of the old data
var oldData = this.data.clone()
/**
* Equations: transmission_probability from a nearby tile:
* $ t = I * Rt$ where I is fire intensity and Rt is the slope term
* $ Rt = exp(0.069 theta) $ theta is the slope angle from -90 to 90 degrees
* this reflects that its hard for fire to spread downhill
* $ theta = atan(dh/w) $ where dh is the difference in height and w is width
* $ theta ~= dh/w $ using the small tan approximation
*
* giving the total equation
* $ t = I * exp(0.069 *dh/w) $
*
* (from Noble et al 1980, DOI: 10.1111/j.1442-9993.1980.tb01243.x)
*/
var fires=0
for (var x = 1; x < this.W-1; x++) {
for (var y = 1; y < this.H-1; y++) {
// can we do this as a convolution?
var fire = oldData.get(x,y,0)*fireMultiplier
var fuel = oldData.get(x,y,1)*fuelMultipler
var ash = oldData.get(x,y,2)
var transmissionProbability = 0
if (fuel==0) continue
ash += fire // fire from last turn causes ash to build up
fuel = _.clamp(fuel-fire,0,fuelMultipler) // and fuel to decrease
if (fire>0){
fire*=fireGrowth // exponentially grow within pixel
} else {
// each neighbouring tile might light it
// intensity of fires in nearby cells
var fires_nearby = oldData.slice([x-1,x+2],[y-1,y+2],[0,1]).reshape(3,3)
// account for diagonal and zero the middle
if (fires_nearby.sum()==0) continue
var width_inv = nj.array([
[1/diagDist,1, 1/diagDist ],
[1, 1e-7, 1 ],
[diagDist, 1, 1/diagDist ]
])
var intensity = nj.multiply(fires_nearby,width_inv)
// Slope spreading term //
// get difference in height ( height is in pixel width units)
var height = oldData.slice([x-1,x+2],[y-1,y+2],[3,4]).reshape(3,3)
var h0 = oldData.get(x,y,3)
var dHeight = nj.subtract(height,h0)
var Rt = nj.exp(nj.multiply(dHeight,width_inv).multiply(0.069))
transmissionProbability = nj.multiply(intensity,Rt)
transmissionProbability = transmissionProbability.mean()
// neighbouring tiles have a chance of lighting our tile
console.assert(transmissionProbability<=1)
if ((transmissionProbability*(fuel**2)*transmissionChance)<Math.random()) {
// nearby_fires=0
continue
}
}
// clamp between 0 and fuel remaining.
fire = _.clamp((fire+transmissionProbability),0,_.min([1,fuel/fuelMultipler]))
// round it so we don't deal with tiny fractions
fire = _.round(fire,2)
console.assert(fire!=undefined)
console.assert(fire!=null)
console.assert(fuel!=undefined)
this.data.set(x,y,0,fire/fireMultiplier)
this.data.set(x,y,1,fuel/fuelMultipler)
this.data.set(x,y,2,ash)
}
}
var t = new Date().getTime()-t0
console.log('tick',this.clock,'time',t)
return t
}
/** push data onto canvases */
display(){
var H = this.W;
var W = this.H;
// first show fire
// Fill in the fire layer
var canvas = document.getElementById('fire')
var ctx = canvas.getContext('2d');
var imageData = ctx.getImageData(0,0,H,W)
var color_scale = d3.interpolateLab('yellow','red')
var fire = this.data.slice(null,null,[0,1])
for (var x = 0; x < imageData.width; x++) {
for (var y = 0; y < imageData.height; y++) {
var d = fire.get(x,y,0)*2
var c = new d3.color(color_scale(d))
var i = (x*H+y)*4
imageData.data[i+0]=c.r
imageData.data[i+1]=c.g
imageData.data[i+2]=c.b
imageData.data[i+3]=_.clamp(d*255*2,0,255)
}
}
ctx.putImageData(imageData, 0, 0);
console.assert(this.data.selection.data.length==imageData.data.length)
// Fill in the fire layer
var canvas = document.getElementById('fuel')
var ctx = canvas.getContext('2d');
var imageData = ctx.getImageData(0,0,H,W)
var color_scale = d3.interpolateLab('white','green')
var fuel = this.data.slice(null,null,[1,2])
for (var x = 0; x < imageData.width; x++) {
for (var y = 0; y < imageData.height; y++) {
var d = fuel.get(x,y,0)*2
var c = new d3.color(color_scale(d))
var i = (x*H+y)*4
imageData.data[i+0]=c.r
imageData.data[i+1]=c.g
imageData.data[i+2]=c.b
imageData.data[i+3]=_.clamp(d*255*2,0,255)
}
}
ctx.putImageData(imageData, 0, 0);
console.assert(this.data.selection.data.length==imageData.data.length)
// Fill in the ash layer
var canvas = document.getElementById('ash')
var ctx = canvas.getContext('2d');
var imageData = ctx.getImageData(0,0,H,W)
var color_scale = d3.interpolateLab('white','grey')
var ash = this.data.slice(null,null,[2,3])
for (var x = 0; x < imageData.width; x++) {
for (var y = 0; y < imageData.height; y++) {
var d = ash.get(x,y,0)*2
var c = new d3.color(color_scale(d))
var i = (x*H+y)*4
imageData.data[i+0]=c.r
imageData.data[i+1]=c.g
imageData.data[i+2]=c.b
imageData.data[i+3]=_.clamp(d*255*2,0,255)
}
}
ctx.putImageData(imageData, 0, 0);
console.assert(this.data.selection.data.length==imageData.data.length)
}
/** this returns stats **/
stats(){
var fire = this.data.slice(null,null,[0,1])
var burning_cells = 0
for (var x = 1; x < this.W-1; x++) {
for (var y = 1; y < this.H-1; y++) {
if (fire.get(x,y,0)!=0) burning_cells++
}
}
return {
fire: _.round(fire.sum(),2),
fuel: _.round(this.data.slice(null,null,[1,2]).sum(),2),
ash: _.round(this.data.slice(null,null,[2,3]).sum(),2),
burning_cells:burning_cells
}
}
start(n=5000){
this.stop()
this.loop = setInterval(()=>{
this.tick()
this.display()
},n)
}
stop(){
if (this.loop) clearInterval(this.loop)
}
}
</script>
<style media="screen">
.leaflet-image-layer {
image-rendering: pixelated
}
</style>
<script src="images/X145.34083_Y-37.533_DT20170428-042328_3857_30/metadata.js"></script>
<script type="text/javascript">
// parameters
var simulations = 2
var fuel_src = 'images/X145.34083_Y-37.533_DT20170428-042328_3857_30/2017.EVI.png'
var elevation_src = 'images/X145.34083_Y-37.533_DT20170428-042328_3857_30/SRTMGL1_003.elevation_pixel_units.png'
var visual = 'images/X145.34083_Y-37.533_DT20170428-042328_3857_30/visual.png'
/**
* Load png images as an array pf [r,g,b,a,r1,g1,b1,a1,...]
* @param {String} image - image url
* @return {Array}
*/
function loadImage(image){
return new Promise(function(resolve, reject) {
// var canvas = document.createElement('canvas');
var img=new Image();
// https://jsfiddle.net/nicolaspanel/047gwg0q/
img.onload = function() {
// load using numpyjs
var data = nj.images.read(img)
resolve(data)
}
img.onerror=reject
img.src=image;
});
}
// load image into canvases
window.onload = function() {
/* leaflet setup */
var southWest = L.latLng(metadata.bounds[0][0],metadata.bounds[0][1]),
northEast = L.latLng(metadata.bounds[1][0],metadata.bounds[1][1]),
bounds = L.latLngBounds(southWest, northEast);
var canvas=document.getElementById('map');
// image_layer1.addTo(leafletMap);
var elevation_layer = new L.ImageOverlay(
canvas.toDataURL(),
bounds,
{opacity:0.5}
)
var fuel_layer = new L.ImageOverlay(
canvas.toDataURL(),
bounds,
{opacity:0.5,attribution:'based on NASA Landsat-8 satellite data'}
)
var ash_layer = new L.ImageOverlay(
canvas.toDataURL(),
bounds,
{opacity:0.5}
)
var fire_layer = new L.ImageOverlay(
canvas.toDataURL(),
bounds,
{opacity:0.5}
)
// image_layer1.addTo(leafletMap);
L.mapbox.accessToken = 'pk.eyJ1IjoiZGlnaXRhbGdsb2JlIiwiYSI6ImNpc2t0ZG16NzA2Y2QydW53M2s0c2liNncifQ.YzEK6kmdCrtvssUfrncwKQ';
var base = L.mapbox.tileLayer('digitalglobe.nal0g75k')
var leafletMap = L.map('leaflet-map',{
center:metadata.point,
zoom:15,
maxBounds:bounds,
// zoomControl: true,
maxZoom:16,
minZoom:4,
layers: [base,ash_layer,fire_layer]
})
baseLayers = {
'DigitalGlobe Maps API: Recent Imagery':base
}
var overlays = {
'fire_layer':fire_layer,
'ash_layer':ash_layer,
'fuel_layer':fuel_layer,
'elevation_layer':elevation_layer
}
L.control.layers(baseLayers, overlays, {collapsed: true}).addTo(leafletMap);
L.control.scale().addTo(leafletMap);
/* there is probobly a better way but this works for now **/
function updateLayers(){
// convert each canvas to a png and load as a map overlay
if (elevation_layer._image) elevation_layer.setUrl(document.getElementById('elevation').toDataURL())
if (fuel_layer._image) fuel_layer.setUrl(document.getElementById('fuel').toDataURL())
if (ash_layer._image) ash_layer.setUrl(document.getElementById('ash').toDataURL())
if (fire_layer._image) fire_layer.setUrl(document.getElementById('fire').toDataURL())
// fire_layer.bringToFront()
}
leafletMap.on('overlayadd',updateLayers)
/* start simulation */
// load elevation onto a canvas
var canvas=document.getElementById('elevation');
var im = new Image()
im.src=visual
im.onload=function(){
// set size of all canvases
document.getElementById('canvases').querySelectorAll('canvas').forEach((canvas)=>{
canvas.width=this.width
canvas.height=this.height
})
var ctx = canvas.getContext('2d');
ctx.drawImage(this, 0, 0);
}
var canvas=document.getElementById('fire');
var prom = Promise.all([
loadImage(fuel_src),
loadImage(elevation_src),
loadImage(elevation_src),
]);
prom.then(([fuel,elev]) => {
// convert to float 32?
fuel.dtype="float32"
fuel=fuel.multiply(1/255)
elev.dtype="float32"
// crop to smallest
var canvas=document.getElementById('fire');
var simulation = new Simulation(
canvas,
{
fuel:fuel,//nj.images.rgb2gray(fuel),
elev:elev,//nj.images.rgb2gray(elev),
}
);
// start simulation
// choose a starting point
// also load a cumulative risk canvas
// for each
// choose starting pixels and display
// now propogate
// repeat
document.getElementById('start').onclick=function(){
simulation.start()
}
document.getElementById('stop').onclick=function(){
simulation.stop()
}
document.getElementById('next').onclick=function(){
simulation.tick()
simulation.display()
console.log(simulation.stats())
updateLayers()
}
document.getElementById('reset').onclick=function(){
simulation = new Simulation(
canvas,
{
fuel:nj.images.rgb2gray(fuel),
elev:nj.images.rgb2gray(elev),
}
);
document.getElementById('tick').innterText="0"
updateLayers()
}
updateLayers()
})
};
</script>
</html>