Mercurial > gemma
comparison pkg/controllers/fwa.go @ 5259:680be197844d
Merged branch new-fwa.
author | Sascha Wilde <wilde@intevation.de> |
---|---|
date | Wed, 13 May 2020 11:28:34 +0200 |
parents | 256ebbeb1252 |
children | ed62f138528a |
comparison
equal
deleted
inserted
replaced
5183:1b5c80ea5582 | 5259:680be197844d |
---|---|
1 // This is Free Software under GNU Affero General Public License v >= 3.0 | |
2 // without warranty, see README.md and license for details. | |
3 // | |
4 // SPDX-License-Identifier: AGPL-3.0-or-later | |
5 // License-Filename: LICENSES/AGPL-3.0.txt | |
6 // | |
7 // Copyright (C) 2018, 2019, 2020 by via donau | |
8 // – Österreichische Wasserstraßen-Gesellschaft mbH | |
9 // Software engineering by Intevation GmbH | |
10 // | |
11 // Author(s): | |
12 // * Sascha L. Teichmann <sascha.teichmann@intevation.de> | |
13 | |
14 package controllers | |
15 | |
16 import ( | |
17 "context" | |
18 "database/sql" | |
19 "encoding/csv" | |
20 "fmt" | |
21 "log" | |
22 "net/http" | |
23 "sort" | |
24 "strconv" | |
25 "strings" | |
26 "time" | |
27 | |
28 "github.com/gorilla/mux" | |
29 | |
30 "gemma.intevation.de/gemma/pkg/common" | |
31 "gemma.intevation.de/gemma/pkg/middleware" | |
32 ) | |
33 | |
34 const ( | |
35 selectBottlenecksLimitingSQL = ` | |
36 SELECT | |
37 lower(validity), | |
38 upper(validity), | |
39 limiting | |
40 FROM | |
41 waterway.bottlenecks | |
42 WHERE | |
43 bottleneck_id = $1 AND | |
44 validity && tstzrange($2, $3)` | |
45 | |
46 selectSymbolBottlenecksSQL = ` | |
47 SELECT | |
48 distinct(b.bottleneck_id) | |
49 FROM | |
50 %s s, waterway.bottlenecks b | |
51 WHERE | |
52 ST_Intersects(b.area, s.area) | |
53 AND s.name = $1 | |
54 AND b.validity && tstzrange($2, $3)` | |
55 | |
56 selectLDCsSQL = ` | |
57 SELECT | |
58 lower(grwl.validity), | |
59 upper(grwl.validity), | |
60 grwl.value | |
61 FROM | |
62 waterway.gauges_reference_water_levels grwl | |
63 JOIN waterway.bottlenecks bns | |
64 ON grwl.location = bns.gauge_location | |
65 WHERE | |
66 grwl.depth_reference like 'LDC%' | |
67 AND bns.bottleneck_id = $1 | |
68 AND grwl.validity && tstzrange($2, $3)` | |
69 | |
70 selectMeasurementsSQL = ` | |
71 WITH data AS ( | |
72 SELECT | |
73 efa.measure_date, | |
74 efa.available_depth_value, | |
75 efa.available_width_value, | |
76 efa.water_level_value | |
77 FROM waterway.effective_fairway_availability efa | |
78 JOIN waterway.fairway_availability fa | |
79 ON efa.fairway_availability_id = fa.id | |
80 JOIN waterway.bottlenecks bn | |
81 ON fa.bottleneck_id = bn.bottleneck_id | |
82 WHERE | |
83 bn.validity @> efa.measure_date AND | |
84 bn.bottleneck_id = $1 AND | |
85 efa.level_of_service = $2 AND | |
86 efa.measure_type = 'Measured' AND | |
87 (efa.available_depth_value IS NOT NULL OR | |
88 efa.available_width_value IS NOT NULL) AND | |
89 efa.water_level_value IS NOT NULL | |
90 ), | |
91 before AS ( | |
92 SELECT * FROM data WHERE measure_date < $3 | |
93 ORDER BY measure_date DESC LIMIT 1 | |
94 ), | |
95 inside AS ( | |
96 SELECT * FROM data WHERE measure_date BETWEEN $3 AND $4 | |
97 ), | |
98 after AS ( | |
99 SELECT * FROM data WHERE measure_date > $4 | |
100 ORDER BY measure_date LIMIT 1 | |
101 ) | |
102 SELECT * FROM before | |
103 UNION ALL | |
104 SELECT * FROM inside | |
105 UNION ALL | |
106 SELECT * FROM after | |
107 ORDER BY measure_date` | |
108 ) | |
109 | |
110 type ( | |
111 timeRange struct { | |
112 lower time.Time | |
113 upper time.Time | |
114 } | |
115 | |
116 ldc struct { | |
117 timeRange | |
118 value []float64 | |
119 } | |
120 | |
121 ldcs []*ldc | |
122 | |
123 limitingFactor int | |
124 | |
125 limitingValidity struct { | |
126 timeRange | |
127 limiting limitingFactor | |
128 ldcs ldcs | |
129 } | |
130 | |
131 limitingValidities []limitingValidity | |
132 | |
133 availMeasurement struct { | |
134 when time.Time | |
135 depth int16 | |
136 width int16 | |
137 value int16 | |
138 } | |
139 | |
140 availMeasurements []availMeasurement | |
141 | |
142 bottleneck struct { | |
143 id string | |
144 validities limitingValidities | |
145 measurements availMeasurements | |
146 } | |
147 | |
148 bottlenecks []bottleneck | |
149 | |
150 fwaMode int | |
151 ) | |
152 | |
153 const ( | |
154 fwaMonthly fwaMode = iota | |
155 fwaQuarterly | |
156 fwaYearly | |
157 ) | |
158 | |
159 const ( | |
160 limitingDepth limitingFactor = iota | |
161 limitingWidth | |
162 ) | |
163 | |
164 var limitingAccess = [...]func(*availMeasurement) float64{ | |
165 limitingDepth: (*availMeasurement).getDepth, | |
166 limitingWidth: (*availMeasurement).getWidth, | |
167 } | |
168 | |
169 // afdRefs are the typical available fairway depth reference values. | |
170 var afdRefs = []float64{ | |
171 230, | |
172 250, | |
173 } | |
174 | |
175 func (ls ldcs) find(from, to time.Time) *ldc { | |
176 for _, l := range ls { | |
177 if l.intersects(from, to) { | |
178 return l | |
179 } | |
180 } | |
181 return nil | |
182 } | |
183 | |
184 func fairwayAvailability(rw http.ResponseWriter, req *http.Request) { | |
185 | |
186 from, to, ok := parseFromTo(rw, req) | |
187 if !ok { | |
188 return | |
189 } | |
190 | |
191 vars := mux.Vars(req) | |
192 name := vars["name"] | |
193 if name == "" { | |
194 http.Error(rw, "missing 'name' parameter.", http.StatusBadRequest) | |
195 return | |
196 } | |
197 | |
198 los, ok := parseFormInt(rw, req, "los", 3) | |
199 if !ok { | |
200 return | |
201 } | |
202 | |
203 ctx := req.Context() | |
204 conn := middleware.GetDBConn(req) | |
205 | |
206 var bns bottlenecks | |
207 var err error | |
208 | |
209 switch vars["kind"] { | |
210 case "bottleneck": | |
211 bns = bottlenecks{{id: name}} | |
212 case "stretch": | |
213 bns, err = loadSymbolBottlenecks(ctx, conn, "users.stretches", name, from, to) | |
214 case "section": | |
215 bns, err = loadSymbolBottlenecks(ctx, conn, "waterway.sections", name, from, to) | |
216 default: | |
217 http.Error(rw, "Invalid kind type.", http.StatusBadRequest) | |
218 return | |
219 } | |
220 | |
221 if err != nil { | |
222 log.Printf("error: %v\n", err) | |
223 http.Error(rw, "cannot extract bottlenecks", http.StatusBadRequest) | |
224 return | |
225 } | |
226 | |
227 // If there are no bottlenecks there is nothing to do. | |
228 if len(bns) == 0 { | |
229 http.Error(rw, "No bottlenecks found.", http.StatusNotFound) | |
230 return | |
231 } | |
232 | |
233 // load validities and limiting factors | |
234 for i := range bns { | |
235 if err := bns[i].loadLimitingValidities(ctx, conn, from, to); err != nil { | |
236 log.Printf("error: %v\n", err) | |
237 http.Error(rw, "cannot load validities", http.StatusInternalServerError) | |
238 return | |
239 } | |
240 // load LCDs | |
241 if err := bns[i].loadLDCs(ctx, conn, from, to); err != nil { | |
242 log.Printf("error: %v\n", err) | |
243 http.Error(rw, "cannot load LDCs", http.StatusInternalServerError) | |
244 return | |
245 } | |
246 // load values | |
247 if err := bns[i].loadValues(ctx, conn, from, to, los); err != nil { | |
248 log.Printf("error: %v\n", err) | |
249 http.Error(rw, "cannot load values", http.StatusInternalServerError) | |
250 return | |
251 } | |
252 } | |
253 | |
254 // separate breaks for depth and width | |
255 var ( | |
256 breaks = parseBreaks(req.FormValue("breaks"), afdRefs) | |
257 depthBreaks = parseBreaks(req.FormValue("depthbreaks"), breaks) | |
258 widthBreaks = parseBreaks(req.FormValue("widthbreaks"), breaks) | |
259 chooseBreaks = [...][]float64{ | |
260 limitingDepth: depthBreaks, | |
261 limitingWidth: widthBreaks, | |
262 } | |
263 | |
264 useDepth = bns.hasLimiting(limitingDepth, from, to) | |
265 useWidth = bns.hasLimiting(limitingWidth, from, to) | |
266 ) | |
267 | |
268 if useDepth && useWidth && len(widthBreaks) != len(depthBreaks) { | |
269 http.Error( | |
270 rw, | |
271 fmt.Sprintf("class breaks lengths differ: %d != %d", | |
272 len(widthBreaks), len(depthBreaks)), | |
273 http.StatusBadRequest, | |
274 ) | |
275 return | |
276 } | |
277 | |
278 availability := vars["type"] == "availability" | |
279 | |
280 var record []string | |
281 if !availability { | |
282 // in days | |
283 record = makeHeader(useDepth && useWidth, 1, breaks, 'd') | |
284 } else { | |
285 // percentage | |
286 record = makeHeader(useDepth && useWidth, 3, breaks, '%') | |
287 } | |
288 | |
289 rw.Header().Add("Content-Type", "text/csv") | |
290 | |
291 out := csv.NewWriter(rw) | |
292 | |
293 if err := out.Write(record); err != nil { | |
294 // Too late for HTTP status message. | |
295 log.Printf("error: %v\n", err) | |
296 return | |
297 } | |
298 | |
299 for i := range record[1:] { | |
300 record[i+1] = "0" | |
301 } | |
302 | |
303 // For every day on every bottleneck we need to find out if this day is valid. | |
304 validities := make([]func(time.Time, time.Time) *limitingValidity, len(bns)) | |
305 for i := range bns { | |
306 validities[i] = bns[i].validities.find() | |
307 } | |
308 | |
309 // Mode reflects if we use monthly, quarterly od yearly intervals. | |
310 mode := parseFWAMode(req.FormValue("mode")) | |
311 | |
312 label, finish := interval(mode, from) | |
313 | |
314 var ( | |
315 totalDays, overLDCDays int | |
316 missingLDCs = make([]int, len(validities)) | |
317 counters = make([]int, len(breaks)+1) | |
318 ) | |
319 | |
320 var current, next time.Time | |
321 | |
322 write := func() error { | |
323 record[0] = label(current) | |
324 | |
325 if !availability { | |
326 record[1] = strconv.Itoa(totalDays - overLDCDays) | |
327 record[2] = strconv.Itoa(overLDCDays) | |
328 for i, c := range counters { | |
329 record[3+i] = strconv.Itoa(c) | |
330 } | |
331 } else { | |
332 overPerc := float64(overLDCDays) * 100 / float64(totalDays) | |
333 record[1] = fmt.Sprintf("%.3f", 100-overPerc) | |
334 record[2] = fmt.Sprintf("%.3f", overPerc) | |
335 for i, c := range counters { | |
336 perc := float64(c) * 100 / float64(totalDays) | |
337 record[3+i] = fmt.Sprintf("%.3f", perc) | |
338 } | |
339 } | |
340 | |
341 return out.Write(record) | |
342 } | |
343 | |
344 // Stop yesterday | |
345 end := common.MinTime(dusk(time.Now()).Add(-time.Nanosecond), to) | |
346 | |
347 // We step through the time in steps of one day. | |
348 for current = from; current.Before(end); { | |
349 | |
350 next = current.AddDate(0, 0, 1) | |
351 | |
352 // Assume that a bottleneck is over LDC. | |
353 overLDC := true | |
354 lowest := len(counters) - 1 | |
355 | |
356 var hasValid bool | |
357 | |
358 // check all bottlenecks | |
359 for i, validity := range validities { | |
360 | |
361 // Check if bottleneck is available for this day. | |
362 vs := validity(current, next) | |
363 if vs == nil { | |
364 continue | |
365 } | |
366 | |
367 // Let's see if we have a LDC for this day. | |
368 ldc := vs.ldcs.find(current, next) | |
369 if ldc == nil { | |
370 missingLDCs[i]++ | |
371 continue | |
372 } | |
373 | |
374 hasValid = true | |
375 | |
376 if overLDC { // If its already not shipable we need no further tests. | |
377 result := bns[i].measurements.classify( | |
378 current, next, | |
379 ldc.value, | |
380 (*availMeasurement).getValue) | |
381 | |
382 if result[1] < 12*time.Hour { | |
383 overLDC = false | |
384 } | |
385 } | |
386 | |
387 if min := minClass(bns[i].measurements.classify( | |
388 current, next, | |
389 chooseBreaks[vs.limiting], | |
390 limitingAccess[vs.limiting]), | |
391 12*time.Hour, | |
392 ); min < lowest { | |
393 lowest = min | |
394 } | |
395 } | |
396 | |
397 if hasValid { | |
398 if overLDC { | |
399 overLDCDays++ | |
400 } | |
401 counters[lowest]++ | |
402 } else { // assume that all is in best conditions | |
403 overLDCDays++ | |
404 counters[len(counters)-1]++ | |
405 } | |
406 | |
407 totalDays++ | |
408 | |
409 if finish(next) { | |
410 if err := write(); err != nil { | |
411 // Too late for HTTP status message. | |
412 log.Printf("error: %v\n", err) | |
413 return | |
414 } | |
415 | |
416 // Reset counters | |
417 overLDCDays, totalDays = 0, 0 | |
418 for i := range counters { | |
419 counters[i] = 0 | |
420 } | |
421 } | |
422 | |
423 current = next | |
424 } | |
425 | |
426 // Write rest if last period was not finished. | |
427 if totalDays > 0 { | |
428 if err := write(); err != nil { | |
429 // Too late for HTTP status message. | |
430 log.Printf("error: %v\n", err) | |
431 return | |
432 } | |
433 } | |
434 | |
435 // TODO: Log missing LDCs | |
436 | |
437 out.Flush() | |
438 if err := out.Error(); err != nil { | |
439 // Too late for HTTP status message. | |
440 log.Printf("error: %v\n", err) | |
441 } | |
442 } | |
443 | |
444 func minClass(classes []time.Duration, threshold time.Duration) int { | |
445 var sum time.Duration | |
446 for i, v := range classes { | |
447 if sum += v; sum >= threshold { | |
448 return i | |
449 } | |
450 } | |
451 return len(classes) - 1 | |
452 } | |
453 | |
454 func dusk(t time.Time) time.Time { | |
455 return time.Date( | |
456 t.Year(), | |
457 t.Month(), | |
458 t.Day(), | |
459 0, 0, 0, 0, | |
460 t.Location()) | |
461 } | |
462 | |
463 func dawn(t time.Time) time.Time { | |
464 return time.Date( | |
465 t.Year(), | |
466 t.Month(), | |
467 t.Day(), | |
468 23, 59, 59, 999999999, | |
469 t.Location()) | |
470 } | |
471 | |
472 func parseFromTo( | |
473 rw http.ResponseWriter, | |
474 req *http.Request, | |
475 ) (time.Time, time.Time, bool) { | |
476 from, ok := parseFormTime(rw, req, "from", time.Now().AddDate(-1, 0, 0)) | |
477 if !ok { | |
478 return time.Time{}, time.Time{}, false | |
479 } | |
480 | |
481 to, ok := parseFormTime(rw, req, "to", from.AddDate(1, 0, 0)) | |
482 if !ok { | |
483 return time.Time{}, time.Time{}, false | |
484 } | |
485 | |
486 from, to = common.OrderTime(from, to) | |
487 // Operate on daily basis so go to full days. | |
488 return dusk(from), dawn(to), true | |
489 } | |
490 | |
491 func parseFWAMode(mode string) fwaMode { | |
492 switch strings.ToLower(mode) { | |
493 case "monthly": | |
494 return fwaMonthly | |
495 case "quarterly": | |
496 return fwaQuarterly | |
497 case "yearly": | |
498 return fwaYearly | |
499 default: | |
500 return fwaMonthly | |
501 } | |
502 } | |
503 | |
504 func breaksToReferenceValue(breaks string) []float64 { | |
505 parts := strings.Split(breaks, ",") | |
506 var values []float64 | |
507 | |
508 for _, part := range parts { | |
509 part = strings.TrimSpace(part) | |
510 if v, err := strconv.ParseFloat(part, 64); err == nil { | |
511 values = append(values, v) | |
512 } | |
513 } | |
514 | |
515 return common.DedupFloat64s(values) | |
516 } | |
517 | |
518 func parseBreaks(breaks string, defaults []float64) []float64 { | |
519 if breaks != "" { | |
520 return breaksToReferenceValue(breaks) | |
521 } | |
522 return defaults | |
523 } | |
524 | |
525 func (tr *timeRange) intersects(from, to time.Time) bool { | |
526 return !(to.Before(tr.lower) || from.After(tr.upper)) | |
527 } | |
528 | |
529 func (tr *timeRange) toUTC() { | |
530 tr.lower = tr.lower.UTC() | |
531 tr.upper = tr.upper.UTC() | |
532 } | |
533 | |
534 func (lvs limitingValidities) find() func(from, to time.Time) *limitingValidity { | |
535 | |
536 var last *limitingValidity | |
537 | |
538 return func(from, to time.Time) *limitingValidity { | |
539 if last != nil && last.intersects(from, to) { | |
540 return last | |
541 } | |
542 for i := range lvs { | |
543 if lv := &lvs[i]; lv.intersects(from, to) { | |
544 last = lv | |
545 return lv | |
546 } | |
547 } | |
548 return nil | |
549 } | |
550 } | |
551 | |
552 func (lvs limitingValidities) hasLimiting(limiting limitingFactor, from, to time.Time) bool { | |
553 for i := range lvs { | |
554 if lvs[i].limiting == limiting && lvs[i].intersects(from, to) { | |
555 return true | |
556 } | |
557 } | |
558 return false | |
559 } | |
560 | |
561 func (bns bottlenecks) hasLimiting(limiting limitingFactor, from, to time.Time) bool { | |
562 for i := range bns { | |
563 if bns[i].validities.hasLimiting(limiting, from, to) { | |
564 return true | |
565 } | |
566 } | |
567 return false | |
568 } | |
569 | |
570 func parseLimitingFactor(limiting string) limitingFactor { | |
571 switch limiting { | |
572 case "depth": | |
573 return limitingDepth | |
574 case "width": | |
575 return limitingWidth | |
576 default: | |
577 log.Printf("warn: unknown limitation '%s'. default to 'depth'\n", limiting) | |
578 return limitingDepth | |
579 } | |
580 } | |
581 | |
582 func loadLimitingValidities( | |
583 ctx context.Context, | |
584 conn *sql.Conn, | |
585 bottleneckID string, | |
586 from, to time.Time, | |
587 ) (limitingValidities, error) { | |
588 | |
589 var lvs limitingValidities | |
590 | |
591 rows, err := conn.QueryContext( | |
592 ctx, | |
593 selectBottlenecksLimitingSQL, | |
594 bottleneckID, | |
595 from, to) | |
596 | |
597 if err != nil { | |
598 return nil, err | |
599 } | |
600 defer rows.Close() | |
601 | |
602 for rows.Next() { | |
603 var ( | |
604 lv limitingValidity | |
605 limiting string | |
606 upper sql.NullTime | |
607 ) | |
608 if err := rows.Scan( | |
609 &lv.lower, | |
610 &upper, | |
611 &limiting, | |
612 ); err != nil { | |
613 return nil, err | |
614 } | |
615 if upper.Valid { | |
616 lv.upper = upper.Time | |
617 } else { | |
618 lv.upper = to.Add(24 * time.Hour) | |
619 } | |
620 lv.toUTC() | |
621 lv.limiting = parseLimitingFactor(limiting) | |
622 lvs = append(lvs, lv) | |
623 } | |
624 | |
625 return lvs, rows.Err() | |
626 } | |
627 | |
628 func loadSymbolBottlenecks( | |
629 ctx context.Context, | |
630 conn *sql.Conn, | |
631 what, name string, | |
632 from, to time.Time, | |
633 ) (bottlenecks, error) { | |
634 | |
635 rows, err := conn.QueryContext( | |
636 ctx, | |
637 fmt.Sprintf(selectSymbolBottlenecksSQL, what), | |
638 name, | |
639 from, to) | |
640 if err != nil { | |
641 return nil, err | |
642 } | |
643 defer rows.Close() | |
644 | |
645 var bns bottlenecks | |
646 | |
647 for rows.Next() { | |
648 var b bottleneck | |
649 if err := rows.Scan(&b.id); err != nil { | |
650 return nil, err | |
651 } | |
652 bns = append(bns, b) | |
653 } | |
654 | |
655 return bns, rows.Err() | |
656 } | |
657 | |
658 func (bn *bottleneck) loadLimitingValidities( | |
659 ctx context.Context, | |
660 conn *sql.Conn, | |
661 from, to time.Time, | |
662 ) error { | |
663 vs, err := loadLimitingValidities( | |
664 ctx, | |
665 conn, | |
666 bn.id, | |
667 from, to) | |
668 if err == nil { | |
669 bn.validities = vs | |
670 } | |
671 return err | |
672 } | |
673 | |
674 func (bn *bottleneck) loadLDCs( | |
675 ctx context.Context, | |
676 conn *sql.Conn, | |
677 from, to time.Time, | |
678 ) error { | |
679 rows, err := conn.QueryContext( | |
680 ctx, selectLDCsSQL, | |
681 bn.id, | |
682 from, to) | |
683 if err != nil { | |
684 return err | |
685 } | |
686 defer rows.Close() | |
687 for rows.Next() { | |
688 l := ldc{value: []float64{0}} | |
689 var upper sql.NullTime | |
690 if err := rows.Scan(&l.lower, &upper, &l.value[0]); err != nil { | |
691 return err | |
692 } | |
693 if upper.Valid { | |
694 l.upper = upper.Time | |
695 } else { | |
696 l.upper = to.Add(24 * time.Hour) | |
697 } | |
698 l.toUTC() | |
699 for i := range bn.validities { | |
700 vs := &bn.validities[i] | |
701 | |
702 if vs.intersects(l.lower, l.upper) { | |
703 vs.ldcs = append(vs.ldcs, &l) | |
704 } | |
705 } | |
706 } | |
707 return rows.Err() | |
708 } | |
709 | |
710 func (bn *bottleneck) loadValues( | |
711 ctx context.Context, | |
712 conn *sql.Conn, | |
713 from, to time.Time, | |
714 los int, | |
715 ) error { | |
716 rows, err := conn.QueryContext( | |
717 ctx, selectMeasurementsSQL, | |
718 bn.id, | |
719 los, | |
720 from, to) | |
721 if err != nil { | |
722 return err | |
723 } | |
724 defer rows.Close() | |
725 | |
726 var ms availMeasurements | |
727 | |
728 for rows.Next() { | |
729 var m availMeasurement | |
730 if err := rows.Scan( | |
731 &m.when, | |
732 &m.depth, | |
733 &m.width, | |
734 &m.value, | |
735 ); err != nil { | |
736 return err | |
737 } | |
738 m.when = m.when.UTC() | |
739 ms = append(ms, m) | |
740 } | |
741 if err := rows.Err(); err != nil { | |
742 return err | |
743 } | |
744 bn.measurements = ms | |
745 return nil | |
746 } | |
747 | |
748 func (measurement *availMeasurement) getDepth() float64 { | |
749 return float64(measurement.depth) | |
750 } | |
751 | |
752 func (measurement *availMeasurement) getValue() float64 { | |
753 return float64(measurement.value) | |
754 } | |
755 | |
756 func (measurement *availMeasurement) getWidth() float64 { | |
757 return float64(measurement.width) | |
758 } | |
759 | |
760 func (measurements availMeasurements) classify( | |
761 from, to time.Time, | |
762 breaks []float64, | |
763 access func(*availMeasurement) float64, | |
764 ) []time.Duration { | |
765 | |
766 if len(breaks) == 0 { | |
767 return []time.Duration{} | |
768 } | |
769 | |
770 result := make([]time.Duration, len(breaks)+1) | |
771 classes := make([]float64, len(breaks)+2) | |
772 values := make([]time.Time, len(classes)) | |
773 | |
774 // Add sentinels | |
775 classes[0] = breaks[0] - 9999 | |
776 classes[len(classes)-1] = breaks[len(breaks)-1] + 9999 | |
777 for i := range breaks { | |
778 classes[i+1] = breaks[i] | |
779 } | |
780 | |
781 idx := sort.Search(len(measurements), func(i int) bool { | |
782 // All values before from can be ignored. | |
783 return !measurements[i].when.Before(from) | |
784 }) | |
785 | |
786 if idx >= len(measurements) { | |
787 return result | |
788 } | |
789 | |
790 // Be safe for interpolation. | |
791 if idx > 0 { | |
792 idx-- | |
793 } | |
794 | |
795 measurements = measurements[idx:] | |
796 | |
797 for i := 0; i < len(measurements)-1; i++ { | |
798 p1 := &measurements[i] | |
799 p2 := &measurements[i+1] | |
800 | |
801 if p1.when.After(to) { | |
802 return result | |
803 } | |
804 | |
805 if p2.when.Before(from) { | |
806 continue | |
807 } | |
808 | |
809 // TODO: Discuss if we want somethinh like this. | |
810 if false && p2.when.Sub(p1.when).Hours() > 1.5 { | |
811 // Don't interpolate ranges bigger then one and a half hour | |
812 continue | |
813 } | |
814 | |
815 lo, hi := common.MaxTime(p1.when, from), common.MinTime(p2.when, to) | |
816 | |
817 m1, m2 := access(p1), access(p2) | |
818 if m1 == m2 { // The whole interval is in only one class. | |
819 for j := 0; j < len(classes)-1; j++ { | |
820 if classes[j] <= m1 && m1 <= classes[j+1] { | |
821 result[j] += hi.Sub(lo) | |
822 break | |
823 } | |
824 } | |
825 continue | |
826 } | |
827 | |
828 f := common.InterpolateTime( | |
829 p1.when, m1, | |
830 p2.when, m2, | |
831 ) | |
832 | |
833 for j, c := range classes { | |
834 values[j] = f(c) | |
835 } | |
836 | |
837 for j := 0; j < len(values)-1; j++ { | |
838 start, end := common.OrderTime(values[j], values[j+1]) | |
839 | |
840 if start.After(hi) || end.Before(lo) { | |
841 continue | |
842 } | |
843 | |
844 start, end = common.MaxTime(start, lo), common.MinTime(end, hi) | |
845 result[j] += end.Sub(start) | |
846 } | |
847 } | |
848 | |
849 return result | |
850 } | |
851 | |
852 func interval(mode fwaMode, t time.Time) ( | |
853 label func(time.Time) string, | |
854 finish func(time.Time) bool, | |
855 ) { | |
856 switch mode { | |
857 case fwaMonthly: | |
858 label, finish = monthLabel, otherMonth(t) | |
859 case fwaQuarterly: | |
860 label, finish = quarterLabel, otherQuarter(t) | |
861 case fwaYearly: | |
862 label, finish = yearLabel, otherYear(t) | |
863 default: | |
864 panic("Unknown mode") | |
865 } | |
866 return | |
867 } | |
868 | |
869 func monthLabel(t time.Time) string { | |
870 return fmt.Sprintf("%02d-%d", t.Month(), t.Year()) | |
871 } | |
872 | |
873 func quarterLabel(t time.Time) string { | |
874 return fmt.Sprintf("Q%d-%d", (int(t.Month())-1)/3+1, t.Year()) | |
875 } | |
876 | |
877 func yearLabel(t time.Time) string { | |
878 return strconv.Itoa(t.Year()) | |
879 } | |
880 | |
881 func otherMonth(t time.Time) func(time.Time) bool { | |
882 return func(x time.Time) bool { | |
883 flag := t.Day() == x.Day() | |
884 if flag { | |
885 t = x | |
886 } | |
887 return flag | |
888 } | |
889 } | |
890 | |
891 func otherQuarter(t time.Time) func(time.Time) bool { | |
892 return func(x time.Time) bool { | |
893 flag := (t.Month()-1)/3 != (x.Month()-1)/3 | |
894 if flag { | |
895 t = x | |
896 } | |
897 return flag | |
898 } | |
899 } | |
900 | |
901 func otherYear(t time.Time) func(time.Time) bool { | |
902 return func(x time.Time) bool { | |
903 flag := t.Year() != x.Year() | |
904 if flag { | |
905 t = x | |
906 } | |
907 return flag | |
908 } | |
909 } | |
910 | |
911 func makeHeader(flag bool, prec int, breaks []float64, unit rune) []string { | |
912 record := make([]string, 1+2+len(breaks)+1) | |
913 record[0] = "# time" | |
914 record[1] = fmt.Sprintf("# < LDC [%c]", unit) | |
915 record[2] = fmt.Sprintf("# >= LDC [%c]", unit) | |
916 for i, v := range breaks { | |
917 if flag { | |
918 if i == 0 { | |
919 record[3] = fmt.Sprintf("# < break_1 [%c]", unit) | |
920 } | |
921 record[i+4] = fmt.Sprintf("# >= break_%d [%c]", i+1, unit) | |
922 } else { | |
923 if i == 0 { | |
924 record[3] = fmt.Sprintf("# < %.*f [%c]", prec, v, unit) | |
925 } | |
926 record[i+4] = fmt.Sprintf("# >= %.*f [%c]", prec, v, unit) | |
927 } | |
928 } | |
929 return record | |
930 } |