UNPKG

igc-xc-score

Version:

igc-xc-score is a paragliding and hang-gliding XC scoring program in vanilla JS

335 lines (319 loc) 13.1 kB
'use strict'; let id = 0; import { Box, Point, Range } from './foundation.js'; export class Solution { constructor(ranges, opt, parent) { if (ranges.length > opt.scoring.cardinality) this.ranges = ranges.slice(0, opt.scoring.cardinality); else this.ranges = ranges; this.opt = opt; this.boxes = []; for (let r = 0; r < this.ranges.length; r++) { // This the left-first ordering of the branch-and-bound // The left-first ordering transforms the permutation into a combination // greatly reducing the number of distinct solutions if (r > 0) if (this.ranges[r - 1].start > this.ranges[r].start) this.ranges[r] = new Range(this.ranges[r - 1].start, this.ranges[r].end); if (r < this.ranges.length - 1) if (this.ranges[r].end > this.ranges[r + 1].end) this.ranges[r] = new Range(this.ranges[r].start, this.ranges[r + 1].end); this.boxes[r] = new Box(this.ranges[r], opt.flight); } this.score = undefined; this.bound = undefined; this.id = id++; if (this.opt.config && this.opt.config.debug) { /* c8 ignore next 3 */ this.parent = parent; this.trace('constructor'); } } do_branch() { let div = 0; // This is the breadth-first ordering of the branch-and-bound // It allows for early cut-off of the huge and obviously impossible branches for (let r = 0; r < this.ranges.length; r++) if (this.ranges[r].count() > this.ranges[div].count()) div = r; for (let r = 0; r < this.ranges.length; r++) if (this.ranges[r].count() > 1 && this.boxes[r].area() > this.boxes[div].area() * 8) div = r; if (this.ranges[div].count() == 1) return []; let subsolutions = []; for (let i of [this.ranges[div].left(), this.ranges[div].right()]) { let subranges = []; for (let r = 0; r < this.ranges.length; r++) if (r != div) subranges[r] = this.ranges[r]; else subranges[r] = i; subsolutions.push(new Solution(subranges, this.opt, this)); } return subsolutions; } do_bound() { this.bound = this.opt.scoring.bound(this.ranges, this.boxes, this.opt); this.trace('bounding'); } do_score() { for (let r = 0; r < this.ranges.length; r++) if (r < this.ranges.length - 1 && this.ranges[r].center() >= this.ranges[r + 1].center()) { this.score = 0; return; } let tp = []; for (let r = 0; r < this.ranges.length; r++) tp[r] = new Point(this.opt.flight.filtered, this.ranges[r].center()); this.scoreInfo = this.opt.scoring.score(tp, this.opt); if (this.opt.scoring.post) { this.opt.scoring.post(this.scoreInfo, this.opt); } this.score = this.scoreInfo.score; this.trace('scoring'); } contentEquals(self, other) { return self.id === other.id; } contentCompare(self, other) { if (self.bound < other.bound) return -1; if (self.bound > other.bound) return 1; if (self.id < other.id) return -1; if (self.id > other.id) return 1; return 0; } /*eslint no-empty: ["off"]*/ geojson() { let features = []; if (this.opt.config && this.opt.config.debug) { /* c8 ignore next 9 */ for (let r = 0; r < this.ranges.length; r++) features.push((new Box(this.ranges[r], this.opt.flight)) .geojson('box' + r, { id: 'box' + r, area: (new Box(this.ranges[r], this.opt.flight)).area(), a: this.ranges[r].start, b: this.ranges[r].end })); } try { const tp = this.scoreInfo.tp; for (const r in tp) { features.push(tp[r] .geojson('tp' + r, { id: 'tp' + r, r: tp[r].r, timestamp: tp[r].r !== undefined ? this.opt.flight.filtered[tp[r].r].timestamp : undefined })); // Skip closing the circuit if there are endpoints if (!(this.scoreInfo.ep && r == tp.length - 1)) features.push({ type: 'Feature', id: 'seg' + r, properties: { id: 'seg' + r, 'stroke': 'yellow', 'stroke-width': 4, d: tp[r].distanceEarth(tp[(r + 1) % tp.length]) }, geometry: { type: 'LineString', coordinates: [[tp[r].x, tp[r].y], [tp[(r + 1) % tp.length].x, tp[(r + 1) % tp.length].y]], style: { 'stroke': 'yellow', 'stroke-width': 4 } } }); } /* c8 ignore next 24 */ if (this.opt.config.debug && this.boxes) { for (const b in this.boxes) { features.push({ type: 'Feature', id: 'box' + b, properties: { id: 'box' + b, 'stroke': 'black', 'stroke-width': 4 }, geometry: { type: 'LineString', coordinates: [ [this.boxes[b].x1, this.boxes[b].y1], [this.boxes[b].x2, this.boxes[b].y1], [this.boxes[b].x2, this.boxes[b].y2], [this.boxes[b].x1, this.boxes[b].y2], [this.boxes[b].x1, this.boxes[b].y1], ], style: { 'stroke': 'black', 'stroke-width': 4 } } }); } } /* c8 ignore next 2 */ } catch (e) { } try { if (this.scoreInfo.ep !== undefined || this.scoreInfo.cp !== undefined) { const cp = this.scoreInfo.cp; const tp = this.scoreInfo.tp; const ep = this.scoreInfo.ep; if (cp && cp['in'] && cp['out']) { for (let r of ['in', 'out']) features.push(cp[r] .geojson('cp_' + r, { id: 'cp_' + r, r: cp[r].r, timestamp: this.opt.flight.filtered[cp[r].r].timestamp })); features.push({ type: 'Feature', id: 'closing', properties: { id: 'closing', 'stroke': 'green', 'stroke-width': 3, d: cp['in'].distanceEarth(cp['out']) }, geometry: { type: 'LineString', coordinates: [[cp['in'].x, cp['in'].y], [cp['out'].x, cp['out'].y]], style: { 'stroke': 'green', 'stroke-width': 3 } } }); } if (ep && ep['start'] && ep['finish']) { for (let r of ['start', 'finish']) features.push(ep[r] .geojson('ep_' + r, { id: 'ep_' + r, r: ep[r].r, timestamp: this.opt.flight.filtered[ep[r].r].timestamp })); features.push({ type: 'Feature', id: 'seg_in', properties: { id: 'seg_in', 'stroke': 'gold', 'stroke-width': 3, d: ep['start'].distanceEarth(tp[0]) }, geometry: { type: 'LineString', coordinates: [[ep['start'].x, ep['start'].y], [tp[0].x, tp[0].y]], style: { 'stroke': 'green', 'stroke-width': 3 } } }); features.push({ type: 'Feature', id: 'seg_out', properties: { id: 'seg_out', 'stroke': 'gold', 'stroke-width': 3, d: ep['finish'].distanceEarth(tp[2]) }, geometry: { type: 'LineString', coordinates: [[tp[2].x, tp[2].y], [ep['finish'].x, ep['finish'].y]], style: { 'stroke': 'green', 'stroke-width': 3 } } }); } } /* c8 ignore next 2 */ } catch (e) { } for (let li = 0; li < this.opt.flight.ll.length; li++) { const l = this.opt.flight.ll[li]; features.push(this.opt.flight.flightPoints[l.launch] .geojson('launch' + li, { id: 'launch' + li, r: l.launch, timestamp: this.opt.flight.filtered[l.launch].timestamp })); features.push(this.opt.flight.flightPoints[l.landing] .geojson('land' + li, { id: 'land' + li, r: l.landing, timestamp: this.opt.flight.filtered[l.landing].timestamp })); } if (!this.opt.config || !this.opt.config.noflight) { let flightData = []; for (let r of this.opt.flight.filtered) { flightData.push([r.longitude, r.latitude]); } features.push({ type: 'Feature', id: 'flight', properties: { id: 'flight' }, geometry: { type: 'LineString', coordinates: flightData } }); } let collection = { type: 'FeatureCollection', properties: { name: 'EPSG:3857', id: this.id, score: this.score !== undefined ? this.score : undefined, bound: this.currentUpperBound !== undefined ? this.currentUpperBound : this.bound, optimal: this.optimal, processedTime: this.time / 1000, processedSolutions: this.processed, type: this.opt.scoring.name, code: this.opt.scoring.code }, features }; return collection; } /* c8 ignore next 14 */ toString() { let s = `${this.opt.scoring.name}`; if (this.score) s += ` ${this.score} points`; if (this.scoreInfo) s += ` ${this.scoreInfo.distance.toFixed(2)}km`; s += ` ( <${this.bound.toFixed(2)} )`; if (this.opt.config && this.opt.config.debug) { s += ` { id: ${this.id} `; for (let r of this.ranges) s += ' ' + r.toString(); s += ' } '; } return s; } /* c8 ignore next 21 */ trace(msg) { if (!this.opt.config.trace || !process.stdout) return; const trace = this.opt.config.trace.split(',').map(str => parseInt(str, 10)); if (trace[0] < 0) { if (this.id % trace[1] !== 0) return; } else { if (this.ranges.some((range, i) => !range.contains(trace[i]))) return; } let r = `${msg ? msg : ''} solution tracing: ${this.id} ${this.opt.scoring.name} `; for (const range of this.ranges) r += range + ' '; if (this.bound) r += `bound: ${this.bound} `; if (this.score) r += `score: ${this.score} `; process.stdout.write('\n' + r + '\n'); this.opt.config.env.fs.writeFileSync(`debug-${this.id}.json`, JSON.stringify(this.geojson({ debug: true }))); } }