diff --git a/simulator/src/simulator-ui.ts b/simulator/src/simulator-ui.ts index bd05761..11f813c 100644 --- a/simulator/src/simulator-ui.ts +++ b/simulator/src/simulator-ui.ts @@ -74,7 +74,7 @@ document.addEventListener('DOMContentLoaded', function() { let resultsContainer = container.querySelector('.simulation-results'); let batteryChargeGraph = new SvgDrawing.SvgElement(resultsContainer.querySelector('.battery-charge-graph svg')); - batteryChargeGraph.viewport.logical = { x: 0, y: 0, width: 365*24, height: parameters.batteryCapacity } + batteryChargeGraph.viewport.setLogical({ x: 0, y: 0, width: 365*24, height: parameters.batteryCapacity }); batteryChargeGraph.graph(simulationResult.batteryLevel); resultsContainer.classList.toggle('is-hidden', false); diff --git a/simulator/src/svg-drawing.ts b/simulator/src/svg-drawing.ts index 9889a11..e8b778b 100644 --- a/simulator/src/svg-drawing.ts +++ b/simulator/src/svg-drawing.ts @@ -6,11 +6,59 @@ namespace SvgDrawing { height: number; } + export interface Point { + x: number; + y: number; + } + + export class Point { + public static add(a: Point, b: Point, out_result: Point) { + out_result.x = a.x + b.x; + out_result.y = a.y + b.y; + } + + public static subtract(a: Point, b: Point, out_result: Point) { + out_result.x = a.x - b.x; + out_result.y = a.y - b.y; + } + + public static normalize(v: Point) { + let invN = 1.0 / Math.sqrt(v.x*v.x + v.y*v.y); + v.x *= invN; + v.y *= invN; + } + + public static dot(a: Point, b: Point) { + return a.x*b.x + a.y*b.y; + } + + public static normalizedDot(a: Point, b: Point) { + let NA = Math.sqrt(a.x*a.x + a.y*a.y); + let NB = Math.sqrt(b.x*b.x + b.y*b.y); + return (a.x*b.x + a.y*b.y) / NA / NB; + } + } + export class Viewport { - constructor(public logical: Rect, public view: Rect) {} + private invLogicalW: number = 0; + private invLogicalH: number = 0; + + constructor(private logical: Rect, private view: Rect) { this.update(); } + + setLogical(r: Rect) { this.logical = r; this.update(); } xLogicalToView(x: number) { return (x - this.logical.x) / this.logical.width * this.view.width + this.view.x; } yLogicalToView(y: number) { return (y - this.logical.y) / this.logical.height * this.view.height + this.view.y; } + + logicalToView(p: Point, out_point: Point) { + out_point.x = (p.x - this.logical.x) * this.invLogicalW * this.view.width + this.view.x; + out_point.y = (p.y - this.logical.y) * this.invLogicalH * this.view.height + this.view.y; + } + + private update() { + this.invLogicalW = 1.0 / this.logical.width; + this.invLogicalH = 1.0 / this.logical.height; + } } export class SvgElement { @@ -25,52 +73,97 @@ namespace SvgDrawing { graph(y: number[]): SVGPathElement; graph(x: number[], y: number[]): SVGPathElement; graph(arg1: number[], arg2?: number[]) { - let x: number[] | null = arg1; - let y: number[] = arg2; - if(!y) { - y = arg1; - x = null; + let read = (idx: number, out_point: Point) => { + out_point.x = arg1[idx]; + out_point.y = arg2[idx]; + return true; + }; + + if(!arg2) { + read = (idx: number, out_point: Point) => { + out_point.x = idx; + out_point.y = arg1[idx]; + return true; + }; } - let num = y.length; - console.assert(!x || num == x.length); + let num = arg1.length; + console.assert(!arg2 || num == arg2.length); if(num <= 1) return null; - let xStep = 6; + let optimizeCurveDist = 5; + let optimizeCurveAngle = 45.0; + if(optimizeCurveDist > 0 || optimizeCurveAngle < 0.0) { + let rawRead = read; + + let dp = Math.cos(optimizeCurveAngle/180*Math.PI); + + let lastDrawnPoint: Point = { x: 0, y: 0 }; + read(0, lastDrawnPoint); + + let nextPoint: Point = { x: 0, y: 0 }; + let dir: Point = { x: 0, y: 0 }; + let nextDir: Point = { x: 0, y: 0 }; + let nextSegDir: Point = { x: 0, y: 0 }; + let perp: Point = { x: 0, y: 0 }; + + read = (idx: number, out_point: Point) => { + rawRead(idx, out_point); + if(idx == 0 || idx == num - 1) return true; + + rawRead(idx + 1, nextPoint); + Point.subtract(out_point, lastDrawnPoint, dir); + Point.subtract(nextPoint, out_point, nextSegDir); + + if(Point.normalizedDot(dir, nextSegDir) < dp) { + lastDrawnPoint = { ...out_point }; + return true; + } + + Point.subtract(nextPoint, lastDrawnPoint, nextDir); + + perp.x = -nextDir.y; + perp.y = nextDir.x; + Point.normalize(perp); + + let d = Math.abs(Point.dot(perp, dir)); + if(d > optimizeCurveDist) { + lastDrawnPoint = { ...out_point }; + return true; + } + + return false; + } + } + + let startTime = performance.now(); + let count = 0; - let coordinates = 'M'+Math.round(this.viewport.xLogicalToView(x? x[0] : 0))+','+Math.round(this.viewport.yLogicalToView(y[0])); + let logicalPoint: Point = { x: 0, y: 0 }; + let viewPoint: Point = { x: 0, y: 0 }; + read(0, logicalPoint); + this.viewport.logicalToView(logicalPoint, viewPoint); + + let coordinates = 'M'+Math.round(viewPoint.x)+','+Math.round(viewPoint.y); coordinates += ' L'; - let lineStartX = x ? x[0] : 0; - let prevX = lineStartX; - let prevY = y[0]; - let yDir = y[1] > y[0] ? 1 : -1; - let count = 0; for(let idx = 0; idx < num; ++idx) { - let isLast = (idx == num - 1); - - let newX = x ? x[idx] : idx; - let newY = y[idx]; - let dir = isLast ? 0 : (y[idx+1] > newY ? 1 : -1); - - if(newX >= lineStartX + xStep || dir != yDir || isLast) { - coordinates += Math.round(this.viewport.xLogicalToView(newX))+','+Math.round(this.viewport.yLogicalToView(newY)); - if(!isLast) coordinates += ' '; - lineStartX = newX; - yDir = isLast ? 0 : (y[idx+1] > newY ? 1 : -1); - ++count; + if(read(idx, logicalPoint)) { + this.viewport.logicalToView(logicalPoint, viewPoint); + coordinates += Math.round(viewPoint.x)+','+Math.round(viewPoint.y)+' '; + count += 1; } - prevY = newY; } - console.log(count); - let path = document.createElementNS('http://www.w3.org/2000/svg','path'); path.setAttribute('class','graph'); path.setAttribute('d', coordinates); this.htmlElement.append(path); + let endTime = performance.now(); + console.log("graph: " + count + " points, " + (endTime - startTime) + "ms"); + return path; } }