Rev 1221 | Details | Compare with Previous | Last modification | View Log | RSS feed
Rev | Author | Line No. | Line |
---|---|---|---|
766 | werner | 1 | /******************************************************************************************** |
2 | ** iLand - an individual based forest landscape and disturbance model |
||
3 | ** http://iland.boku.ac.at |
||
4 | ** Copyright (C) 2009- Werner Rammer, Rupert Seidl |
||
5 | ** |
||
6 | ** This program is free software: you can redistribute it and/or modify |
||
7 | ** it under the terms of the GNU General Public License as published by |
||
8 | ** the Free Software Foundation, either version 3 of the License, or |
||
9 | ** (at your option) any later version. |
||
10 | ** |
||
11 | ** This program is distributed in the hope that it will be useful, |
||
12 | ** but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
13 | ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
14 | ** GNU General Public License for more details. |
||
15 | ** |
||
16 | ** You should have received a copy of the GNU General Public License |
||
17 | ** along with this program. If not, see <http://www.gnu.org/licenses/>. |
||
18 | ********************************************************************************************/ |
||
19 | #include "spatialanalysis.h" |
||
20 | |||
21 | #include "globalsettings.h" |
||
22 | #include "model.h" |
||
1017 | werner | 23 | #include "tree.h" |
24 | #include "stamp.h" |
||
767 | werner | 25 | #include "helper.h" |
1017 | werner | 26 | #include "resourceunit.h" |
1083 | werner | 27 | #include "scriptgrid.h" |
766 | werner | 28 | |
793 | werner | 29 | #include <QJSEngine> |
30 | #include <QJSValue> |
||
767 | werner | 31 | void SpatialAnalysis::addToScriptEngine() |
32 | { |
||
33 | SpatialAnalysis *spati = new SpatialAnalysis; |
||
793 | werner | 34 | QJSValue v = GlobalSettings::instance()->scriptEngine()->newQObject(spati); |
767 | werner | 35 | GlobalSettings::instance()->scriptEngine()->globalObject().setProperty("SpatialAnalysis", v); |
36 | } |
||
766 | werner | 37 | |
767 | werner | 38 | SpatialAnalysis::~SpatialAnalysis() |
766 | werner | 39 | { |
767 | werner | 40 | if (mRumple) |
41 | delete mRumple; |
||
766 | werner | 42 | } |
43 | |||
767 | werner | 44 | double SpatialAnalysis::rumpleIndexFullArea() |
45 | { |
||
46 | if (!mRumple) |
||
47 | mRumple = new RumpleIndex; |
||
48 | double rum = mRumple->value(); |
||
49 | return rum; |
||
50 | } |
||
766 | werner | 51 | |
1080 | werner | 52 | /// extract patches (clumps) from the grid 'src'. |
53 | /// Patches are defined as adjacent pixels (8-neighborhood) |
||
54 | /// Return: vector with number of pixels per patch (first element: patch 1, second element: patch 2, ...) |
||
1085 | werner | 55 | QList<int> SpatialAnalysis::extractPatches(Grid<double> &src, int min_size, QString fileName) |
1080 | werner | 56 | { |
1088 | werner | 57 | mClumpGrid.setup(src.metricRect(), src.cellsize()); |
1080 | werner | 58 | mClumpGrid.wipe(); |
59 | |||
60 | // now loop over all pixels and run a floodfill algorithm |
||
61 | QPoint start; |
||
62 | QQueue<QPoint> pqueue; // for the flood fill algorithm |
||
1085 | werner | 63 | QList<int> counts; |
1080 | werner | 64 | int patch_index = 0; |
65 | int total_size = 0; |
||
1083 | werner | 66 | int patches_skipped = 0; |
1080 | werner | 67 | for (int i=0;i<src.count();++i) { |
68 | if (src[i]>0. && mClumpGrid[i]==0) { |
||
69 | start = src.indexOf(i); |
||
70 | pqueue.clear(); |
||
71 | patch_index++; |
||
72 | |||
73 | // quick and dirty implementation of the flood fill algroithm. |
||
74 | // based on: http://en.wikipedia.org/wiki/Flood_fill |
||
75 | // returns the number of pixels colored |
||
76 | |||
77 | pqueue.enqueue(start); |
||
78 | int found = 0; |
||
79 | while (!pqueue.isEmpty()) { |
||
80 | QPoint p = pqueue.dequeue(); |
||
81 | if (!src.isIndexValid(p)) |
||
82 | continue; |
||
1081 | werner | 83 | if (src.valueAtIndex(p)>0. && mClumpGrid.valueAtIndex(p) == 0) { |
1080 | werner | 84 | mClumpGrid.valueAtIndex(p) = patch_index; |
85 | pqueue.enqueue(p+QPoint(-1,0)); |
||
86 | pqueue.enqueue(p+QPoint(1,0)); |
||
87 | pqueue.enqueue(p+QPoint(0,-1)); |
||
88 | pqueue.enqueue(p+QPoint(0,1)); |
||
89 | pqueue.enqueue(p+QPoint(1,1)); |
||
90 | pqueue.enqueue(p+QPoint(-1,1)); |
||
91 | pqueue.enqueue(p+QPoint(-1,-1)); |
||
92 | pqueue.enqueue(p+QPoint(1,-1)); |
||
93 | ++found; |
||
94 | } |
||
95 | } |
||
1083 | werner | 96 | if (found<min_size) { |
97 | // delete the patch again |
||
98 | pqueue.enqueue(start); |
||
99 | while (!pqueue.isEmpty()) { |
||
100 | QPoint p = pqueue.dequeue(); |
||
101 | if (!src.isIndexValid(p)) |
||
102 | continue; |
||
103 | if (mClumpGrid.valueAtIndex(p) == patch_index) { |
||
104 | mClumpGrid.valueAtIndex(p) = -1; |
||
105 | pqueue.enqueue(p+QPoint(-1,0)); |
||
106 | pqueue.enqueue(p+QPoint(1,0)); |
||
107 | pqueue.enqueue(p+QPoint(0,-1)); |
||
108 | pqueue.enqueue(p+QPoint(0,1)); |
||
109 | pqueue.enqueue(p+QPoint(1,1)); |
||
110 | pqueue.enqueue(p+QPoint(-1,1)); |
||
111 | pqueue.enqueue(p+QPoint(-1,-1)); |
||
112 | pqueue.enqueue(p+QPoint(1,-1)); |
||
113 | } |
||
114 | } |
||
115 | --patch_index; |
||
116 | patches_skipped++; |
||
117 | |||
118 | } else { |
||
119 | // save the patch in the result |
||
120 | counts.push_back(found); |
||
121 | total_size+=found; |
||
122 | } |
||
1080 | werner | 123 | } |
124 | } |
||
1085 | werner | 125 | // remove the -1 again... |
126 | mClumpGrid.limit(0,999999); |
||
127 | |||
1083 | werner | 128 | qDebug() << "extractPatches: found" << patch_index << "patches, total valid pixels:" << total_size << "skipped" << patches_skipped; |
1080 | werner | 129 | if (!fileName.isEmpty()) { |
130 | qDebug() << "extractPatches: save to file:" << GlobalSettings::instance()->path(fileName); |
||
131 | Helper::saveToTextFile(GlobalSettings::instance()->path(fileName), gridToESRIRaster(mClumpGrid) ); |
||
132 | } |
||
133 | return counts; |
||
134 | |||
135 | } |
||
136 | |||
767 | werner | 137 | void SpatialAnalysis::saveRumpleGrid(QString fileName) |
138 | { |
||
139 | if (!mRumple) |
||
140 | mRumple = new RumpleIndex; |
||
141 | |||
1017 | werner | 142 | Helper::saveToTextFile(GlobalSettings::instance()->path(fileName), gridToESRIRaster(mRumple->rumpleGrid()) ); |
767 | werner | 143 | |
144 | } |
||
145 | |||
1017 | werner | 146 | void SpatialAnalysis::saveCrownCoverGrid(QString fileName) |
147 | { |
||
148 | calculateCrownCover(); |
||
149 | Helper::saveToTextFile(GlobalSettings::instance()->path(fileName), gridToESRIRaster(mCrownCoverGrid) ); |
||
767 | werner | 150 | |
1017 | werner | 151 | } |
152 | |||
1083 | werner | 153 | QJSValue SpatialAnalysis::patches(QJSValue grid, int min_size) |
154 | { |
||
155 | ScriptGrid *sg = qobject_cast<ScriptGrid*>(grid.toQObject()); |
||
156 | if (sg) { |
||
157 | // extract patches (keep patches with a size >= min_size |
||
1085 | werner | 158 | mLastPatches = extractPatches(*sg->grid(), min_size, QString()); |
1083 | werner | 159 | // create a (double) copy of the internal clump grid, and return this grid |
160 | // as a JS value |
||
161 | QJSValue v = ScriptGrid::createGrid(mClumpGrid.toDouble(),"patch"); |
||
162 | return v; |
||
163 | } |
||
164 | return QJSValue(); |
||
165 | } |
||
166 | |||
1017 | werner | 167 | void SpatialAnalysis::calculateCrownCover() |
168 | { |
||
169 | mCrownCoverGrid.setup(GlobalSettings::instance()->model()->RUgrid().metricRect(), |
||
170 | GlobalSettings::instance()->model()->RUgrid().cellsize()); |
||
171 | |||
172 | // calculate the crown cover per resource unit. We use the "reader"-stamps of the individual trees |
||
173 | // as they represent the crown (size). We also simply hijack the LIF grid for our calculations. |
||
174 | FloatGrid *grid = GlobalSettings::instance()->model()->grid(); |
||
175 | grid->initialize(0.f); |
||
176 | // we simply iterate over all trees of all resource units (not bothering about multithreading here) |
||
177 | int x,y; |
||
178 | AllTreeIterator ati(GlobalSettings::instance()->model()); |
||
179 | while (Tree *t = ati.nextLiving()) { |
||
180 | // apply the reader-stamp |
||
181 | const Stamp *reader = t->stamp()->reader(); |
||
182 | QPoint pos_reader = t->positionIndex(); // tree position |
||
183 | pos_reader-=QPoint(reader->offset(), reader->offset()); |
||
184 | int reader_size = reader->size(); |
||
185 | int rx = pos_reader.x(); |
||
186 | int ry = pos_reader.y(); |
||
1019 | werner | 187 | // the reader stamps are stored such as to have a sum of 1.0 over all pixels |
188 | // (i.e.: they express the percentage for each cell contributing to the full crown). |
||
189 | // we thus calculate a the factor to "blow up" cell values; a fully covered cell has then a value of 1, |
||
190 | // and values between 0-1 are cells that are partially covered by the crown. |
||
1020 | werner | 191 | double crown_factor = reader->crownArea()/double(cPxSize*cPxSize); |
1017 | werner | 192 | |
193 | // add the reader-stamp values: multiple (partial) crowns can add up to being fully covered |
||
194 | for (y=0;y<reader_size; ++y) { |
||
195 | for (x=0;x<reader_size;++x) { |
||
1019 | werner | 196 | grid->valueAtIndex(rx+x, ry+y) += (*reader)(x,y)*crown_factor; |
1017 | werner | 197 | } |
198 | } |
||
199 | } |
||
200 | // now aggregate values for each resource unit |
||
201 | Model *model = GlobalSettings::instance()->model(); |
||
202 | for (float *rg = mCrownCoverGrid.begin(); rg!=mCrownCoverGrid.end();++rg) { |
||
203 | ResourceUnit *ru = model->RUgrid().constValueAtIndex(mCrownCoverGrid.indexOf(rg)); |
||
1022 | werner | 204 | if (!ru) { |
205 | *rg=0.f; |
||
1017 | werner | 206 | continue; |
1022 | werner | 207 | } |
1017 | werner | 208 | float cc_sum = 0.f; |
209 | GridRunner<float> runner(grid, mCrownCoverGrid.cellRect(mCrownCoverGrid.indexOf(rg))); |
||
210 | while (float *gv = runner.next()) { |
||
211 | if (model->heightGridValue(runner.currentIndex().x(), runner.currentIndex().y()).isValid()) |
||
1021 | werner | 212 | if (*gv >= 0.5f) // 0.5: half of a 2m cell is covered by a tree crown; is a bit pragmatic but seems reasonable (and works) |
1017 | werner | 213 | cc_sum++; |
214 | } |
||
215 | if (ru->stockableArea()>0.) { |
||
1018 | werner | 216 | double value = cPxSize*cPxSize*cc_sum/ru->stockableArea(); |
1017 | werner | 217 | *rg = limit(value, 0., 1.); |
218 | } |
||
219 | } |
||
220 | } |
||
221 | |||
222 | |||
766 | werner | 223 | /****************************************************************************************/ |
224 | /********************************* RumpleIndex class ************************************/ |
||
225 | /****************************************************************************************/ |
||
226 | |||
227 | |||
228 | void RumpleIndex::setup() |
||
229 | { |
||
230 | mRumpleGrid.clear(); |
||
231 | if (!GlobalSettings::instance()->model()) return; |
||
232 | |||
233 | // the rumple grid hast the same dimensions as the resource unit grid (i.e. 100 meters) |
||
234 | mRumpleGrid.setup(GlobalSettings::instance()->model()->RUgrid().metricRect(), |
||
235 | GlobalSettings::instance()->model()->RUgrid().cellsize()); |
||
236 | |||
237 | } |
||
238 | |||
239 | void RumpleIndex::calculate() |
||
240 | { |
||
241 | if (mRumpleGrid.isEmpty()) |
||
242 | setup(); |
||
243 | |||
244 | mRumpleGrid.initialize(0.f); |
||
245 | HeightGrid *hg = GlobalSettings::instance()->model()->heightGrid(); |
||
246 | |||
247 | // iterate over the resource units and calculate the rumple index / surface area for each resource unit |
||
248 | HeightGridValue* hgv_8[8]; // array holding pointers to height grid values (neighborhood) |
||
249 | float heights[9]; // array holding heights (8er neighborhood + center pixel) |
||
250 | int total_valid_pixels = 0; |
||
251 | float total_surface_area = 0.f; |
||
252 | for (float *rg = mRumpleGrid.begin(); rg!=mRumpleGrid.end();++rg) { |
||
253 | int valid_pixels = 0; |
||
254 | float surface_area_sum = 0.f; |
||
255 | GridRunner<HeightGridValue> runner(hg, mRumpleGrid.cellRect(mRumpleGrid.indexOf(rg))); |
||
256 | while (runner.next()) { |
||
257 | if (runner.current()->isValid()) { |
||
258 | runner.neighbors8(hgv_8); |
||
259 | bool valid = true; |
||
260 | float *hp = heights; |
||
261 | *hp++ = runner.current()->height; |
||
262 | // retrieve height values from the grid |
||
263 | for (int i=0;i<8;++i) { |
||
264 | *hp++ = hgv_8[i] ? hgv_8[i]->height: 0; |
||
265 | if (hgv_8[i] && !hgv_8[i]->isValid()) valid = false; |
||
266 | if (!hgv_8[i]) valid = false; |
||
267 | } |
||
268 | // calculate surface area only for cells which are (a) within the project area, and (b) all neighboring pixels are inside the forest area |
||
269 | if (valid) { |
||
270 | valid_pixels++; |
||
271 | float surface_area = calculateSurfaceArea(heights, hg->cellsize()); |
||
272 | surface_area_sum += surface_area; |
||
273 | } |
||
274 | } |
||
275 | } |
||
276 | if (valid_pixels>0) { |
||
277 | float rumple_index = surface_area_sum / ( (float) valid_pixels * hg->cellsize()*hg->cellsize()); |
||
278 | *rg = rumple_index; |
||
279 | total_valid_pixels += valid_pixels; |
||
280 | total_surface_area += surface_area_sum; |
||
281 | } |
||
282 | } |
||
283 | mRumpleIndex = 0.; |
||
284 | if (total_valid_pixels>0) { |
||
285 | float rumple_index = total_surface_area / ( (float) total_valid_pixels * hg->cellsize()*hg->cellsize()); |
||
286 | mRumpleIndex = rumple_index; |
||
287 | } |
||
288 | mLastYear = GlobalSettings::instance()->currentYear(); |
||
289 | } |
||
290 | |||
291 | double RumpleIndex::value(const bool force_recalculate) |
||
292 | { |
||
293 | if (force_recalculate || mLastYear != GlobalSettings::instance()->currentYear()) |
||
294 | calculate(); |
||
295 | return mRumpleIndex; |
||
296 | } |
||
297 | |||
298 | double RumpleIndex::test_triangle_area() |
||
299 | { |
||
300 | // check calculation: numbers for Jenness paper |
||
301 | float hs[9]={165, 170, 145, 160, 183, 155,122,175,190}; |
||
302 | double area = calculateSurfaceArea(hs, 100); |
||
303 | return area; |
||
304 | } |
||
305 | |||
306 | inline double surface_length(float h1, float h2, float l) |
||
307 | { |
||
308 | return sqrt((h1-h2)*(h1-h2) + l*l); |
||
309 | } |
||
310 | inline double heron_triangle_area(float a, float b, float c) |
||
311 | { |
||
312 | float s=(a+b+c)/2.f; |
||
313 | return sqrt(s * (s-a) * (s-b) * (s-c) ); |
||
314 | } |
||
315 | |||
316 | /// calculate the surface area of a pixel given its height value, the height of the 8 neigboring pixels, and the cellsize |
||
317 | /// the algorithm is based on http://www.jennessent.com/downloads/WSB_32_3_Jenness.pdf |
||
318 | double RumpleIndex::calculateSurfaceArea(const float *heights, const float cellsize) |
||
319 | { |
||
320 | // values in the height array [0..8]: own height / north/east/west/south/ NE/NW/SE/SW |
||
321 | // step 1: calculate length on 3d surface between all edges |
||
322 | // 8(A) * 1(B) * 5(C) <- 0: center cell, indices in the "heights" grid, A..I: codes used by Jenness |
||
323 | // 4(D) * 0(E) * 2(F) |
||
324 | // 7(G) * 3(H) * 6(I) |
||
325 | |||
326 | float slen[16]; // surface lengths (divided by 2) |
||
327 | // horizontal |
||
328 | slen[0] = surface_length(heights[8], heights[1], cellsize)/2.f; |
||
329 | slen[1] = surface_length(heights[1], heights[5], cellsize)/2.f; |
||
330 | slen[2] = surface_length(heights[4], heights[0], cellsize)/2.f; |
||
331 | slen[3] = surface_length(heights[0], heights[2], cellsize)/2.f; |
||
332 | slen[4] = surface_length(heights[7], heights[3], cellsize)/2.f; |
||
333 | slen[5] = surface_length(heights[3], heights[6], cellsize)/2.f; |
||
334 | // vertical |
||
335 | slen[6] = surface_length(heights[8], heights[4], cellsize)/2.f; |
||
336 | slen[7] = surface_length(heights[1], heights[0], cellsize)/2.f; |
||
337 | slen[8] = surface_length(heights[5], heights[2], cellsize)/2.f; |
||
338 | slen[9] = surface_length(heights[4], heights[7], cellsize)/2.f; |
||
339 | slen[10] = surface_length(heights[0], heights[3], cellsize)/2.f; |
||
340 | slen[11] = surface_length(heights[2], heights[6], cellsize)/2.f; |
||
341 | // diagonal |
||
342 | float cellsize_diag = cellsize * M_SQRT2; |
||
343 | slen[12] = surface_length(heights[0], heights[8], cellsize_diag)/2.f; |
||
344 | slen[13] = surface_length(heights[0], heights[5], cellsize_diag)/2.f; |
||
345 | slen[14] = surface_length(heights[0], heights[7], cellsize_diag)/2.f; |
||
346 | slen[15] = surface_length(heights[0], heights[6], cellsize_diag)/2.f; |
||
347 | |||
348 | // step 2: combine the three sides of all the 8 sub triangles using Heron's formula |
||
349 | double surface_area = 0.; |
||
350 | surface_area += heron_triangle_area(slen[12], slen[0], slen[7]); // i |
||
351 | surface_area += heron_triangle_area(slen[7], slen[1], slen[13]); // ii |
||
352 | surface_area += heron_triangle_area(slen[6], slen[2], slen[12]); // iii |
||
353 | surface_area += heron_triangle_area(slen[13], slen[8], slen[3]); // iv |
||
354 | surface_area += heron_triangle_area(slen[2], slen[9], slen[14]); // v |
||
355 | surface_area += heron_triangle_area(slen[3], slen[11], slen[15]); // vi |
||
356 | surface_area += heron_triangle_area(slen[14], slen[10], slen[4]); // vii |
||
357 | surface_area += heron_triangle_area(slen[10], slen[15], slen[5]); // viii |
||
358 | |||
359 | return surface_area; |
||
360 | } |
||
767 | werner | 361 | |
362 | /* *************************************************************************************** */ |
||
363 | /* ******************************* Spatial Layers **************************************** */ |
||
364 | /* *************************************************************************************** */ |
||
365 | void SpatialLayeredGrid::setup() |
||
366 | { |
||
367 | addGrid("rumple", 0); |
||
368 | } |
||
369 | |||
370 | void SpatialLayeredGrid::createGrid(const int grid_index) |
||
371 | { |
||
1034 | werner | 372 | Q_UNUSED(grid_index); // TODO: what should happen here? |
767 | werner | 373 | } |
374 | |||
375 | |||
376 | int SpatialLayeredGrid::addGrid(const QString name, FloatGrid *grid) |
||
377 | { |
||
378 | mGridNames.append(name); |
||
379 | mGrids.append(grid); |
||
777 | werner | 380 | return mGrids.size(); |
767 | werner | 381 | } |
382 | |||
383 | |||
384 |