changeset 16:9a609bcf0809

Port main script to TypeScript and prepare for serving it.
author Paul Fisher <paul@pfish.zone>
date Sat, 12 Oct 2019 14:03:52 -0400
parents df3e0534c994
children fdb874e0b270
files weather_server/server.py weather_server/static/script.js weather_server/templates/location.html weather_server/typescript/app/apis.d.ts weather_server/typescript/app/graph.ts weather_server/typescript/app/math.ts weather_server/typescript/app/tsconfig.json
diffstat 7 files changed, 262 insertions(+), 178 deletions(-) [+]
line wrap: on
line diff
--- a/weather_server/server.py	Fri Oct 11 20:50:50 2019 -0400
+++ b/weather_server/server.py	Sat Oct 12 14:03:52 2019 -0400
@@ -89,7 +89,10 @@
         ]
         resp = flask.Response()
         resp.content_type = 'application/json'
-        resp.data = common.json_dumps(readings)
+        resp.data = common.json_dumps({
+            'timezone': loc.tz_name,
+            'readings': readings,
+        })
         return resp
 
     return app
--- a/weather_server/static/script.js	Fri Oct 11 20:50:50 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,174 +0,0 @@
-'use strict';
-
-/**
- * Converts Celsius to Fahrenheit.
- * @param {number} tempC
- * @return {number} The temperature in Fahrenheit.
- */
-function cToF(tempC) {
-    return tempC * 9 / 5 + 32;
-}
-
-const MAGNUS_B = 17.62;
-const MAGNUS_C = 243.12;
-
-/**
- * The gamma function to calculate dew point.
- *
- * @param {number} tempC The temperature, in degrees Celsius.
- * @param {number} rhPct The relative humidity, in percent.
- * @return {number} The value of the gamma function.
- */
-function gammaFn(tempC, rhPct) {
-    return Math.log(rhPct / 100) + MAGNUS_B * tempC / (MAGNUS_C + tempC);
-}
-
-/**
- * Calculates the dew point.
- *
- * @param {number} tempC The temperature, in degrees Celsius.
- * @param {number} rhPct The relative humidity, in percent.
- * @return {number} The dew point, in degrees Celsius.
- */
-function dewPointC(tempC, rhPct) {
-    const gamma = gammaFn(tempC, rhPct);
-    return MAGNUS_C * gamma / (MAGNUS_B - gamma);
-}
-
-const HISTORY_SECONDS = 86400;
-
-/**
- * Sets up everything.
- * @param {HTMLElement} tempElement The element where temperature data is.
- * @param {HTMLElement} dewPointElement The element where the dew point is.
- */
-async function setUp(tempElement, dewPointElement) {
-    const nowTS = new Date().getTime() / 1000;
-    const startTS = nowTS - HISTORY_SECONDS;
-    const query = new URL(location.href);
-    query.pathname = query.pathname + '/recent';
-    query.search = '';
-    query.searchParams.set('seconds', String(HISTORY_SECONDS));
-    const results = await fetch(query.href);
-    if (!results.ok) return;
-    const data = await results.json();
-    if (data.length === 0) return;
-
-    const tempsF = data.map(s => [s.sample_time, cToF(s.temp_c)]);
-    const dewPointsF = data.map(
-        s => [s.sample_time, cToF(dewPointC(s.temp_c, s.rh_pct))]);
-    setUpElement(tempElement, [startTS, nowTS], tempsF);
-    setUpElement(dewPointElement, [startTS, nowTS], dewPointsF);
-}
-
-/**
- * Sets up charting for this element.
- * @param {HTMLElement} element The element to put a graph in.
- * @param {[number, number]} timeRange The `[start, end]` of the time range.
- * @param {[number, number][]} data The data to chart.
- */
-function setUpElement(element, timeRange, data) {
-    element.insertBefore(document.createElement('canvas'), element.firstChild);
-    element.classList.remove('plain');
-    element.classList.add('fancy');
-    const doDraw = () => redrawCanvas(element, data, timeRange);
-    doDraw();
-    addEventListener('resize', doDraw);
-}
-
-/**
- *
- * @param {HTMLElement} element The parent element to put the `<canvas>` in.
- * @param {[number, number][]} data The data to chart.
- * @param {[number, number]} xRange The `[start, end]` of the X range to plot.
- */
-function redrawCanvas(element, data, xRange) {
-    let canvas = element.getElementsByTagName('canvas')[0];
-    if (!canvas) {
-        canvas = document.createElement('canvas');
-        element.insertBefore(canvas, element.firstChild);
-    }
-    const dpr = window.devicePixelRatio || 1;
-    const cssSize = element.getBoundingClientRect();
-    const pxSize = [cssSize.width * dpr, cssSize.height * dpr];
-    canvas.width = pxSize[0];
-    canvas.height = pxSize[1];
-    const ctx = canvas.getContext('2d');
-    ctx.clearRect(0, 0, pxSize[0], pxSize[1]);
-    const computed = getComputedStyle(element);
-    ctx.strokeStyle = computed.color;
-    drawChart(ctx, data, xRange, pxSize);
-}
-
-/** The height of the chart in degrees. */
-const CHART_RANGE_DEGREES = 15;
-
-/**
- * Charts some data.
- *
- * @param {CanvasRenderingContext2D} ctx The context to draw in.
- * @param {[number, number][]} data The data to chart, as `[x, y]` pairs.
- * @param {[number, number]} xRange The bounds of the X axis to draw.
- * @param {[number, number]} size The `[width, height]` of the context.
- */
-function drawChart(ctx, data, xRange, size) {
-    const yRange = calculateYRange(data.map(d => d[1]));
-
-    ctx.lineWidth = 1.5;
-
-    ctx.beginPath();
-    const first = project(data[0], size, xRange, yRange);
-    ctx.moveTo(...first);
-    for (const pt of data) {
-        const projected = project(pt, size, xRange, yRange);
-        ctx.lineTo(...projected);
-    }
-    ctx.stroke();
-}
-
-/**
- * Determines what the Y range of the chart should be.
- * @param {number[]} ys The Y values of the chart.
- * @return {[number, number]} The lowest and highest values of the range.
- */
-function calculateYRange(ys) {
-    const yMax = Math.max(...ys);
-    const yMin = Math.min(...ys);
-    const yMid = (yMin + yMax) / 2;
-    const lastY = ys[ys.length - 1];
-
-    // We want the last value to be, at most, at the top or bottom 1/4 line
-    // of the chart.
-
-    // If the middle of the range is already close enough, just use that.
-    if (CHART_RANGE_DEGREES / 4 <= Math.abs(yMid - lastY)) {
-        return [yMid - CHART_RANGE_DEGREES / 2, yMid + CHART_RANGE_DEGREES / 2];
-    }
-    // Otherwise, clamp the chart range.
-    if (lastY < yMid) {
-        return [lastY - CHART_RANGE_DEGREES / 4, lastY + 3 * CHART_RANGE_DEGREES / 4];
-    }
-    return [lastY - 3 * CHART_RANGE_DEGREES / 4, lastY + CHART_RANGE_DEGREES / 4];
-}
-
-/**
- * Projects a Cartesian coordinate into Canvas space.
- *
- * @param {[number, number]} coord The `[x, y]` coordinate to project.
- * @param {[number, number]} size The `[width, height]` of the context.
- * @param {[number, number]} xRange The range of X values in the context.
- * @param {[number, number]} yRange The range of Y values in the context.
- * @return {[number, number]} The `[x, y]` coordinate in Canvas space.
- */
-function project(coord, size, xRange, yRange) {
-    const [x, y] = coord;
-    const [xMin, xMax] = xRange;
-    const xSpan = xMax - xMin;
-    const [yMin, yMax] = yRange;
-    const ySpan = yMax - yMin;
-    const [xSize, ySize] = size;
-    return [
-        (x - xMin) / xSpan * xSize,
-        (yMax- y) / ySpan * ySize,
-    ]
-}
--- a/weather_server/templates/location.html	Fri Oct 11 20:50:50 2019 -0400
+++ b/weather_server/templates/location.html	Sat Oct 12 14:03:52 2019 -0400
@@ -39,13 +39,15 @@
         {% else %}
             <p id="big-question-mark">?</p>
         {% endif %}
-        <script src="{{ url_for('static', filename='script.js') }}"></script>
         <!--
+            <script src="{{ url_for('static', filename='amd/mad.js') }}"></script>
+            <script>new MADRegistry().install(self);</script>
+            <script src="{{ url_for('static', filename='graph.js') }}"></script>
             <script>
-                setUp(
+                require('graph').setUp(
                     document.getElementById('temp'),
                     document.getElementById('dewpoint'));
             </script>
-        -->
+        <!-- -->
     </body>
 </html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/weather_server/typescript/app/apis.d.ts	Sat Oct 12 14:03:52 2019 -0400
@@ -0,0 +1,18 @@
+export interface WeatherResponse {
+    /** tzdata name of the sensor's timezone. */
+    timezone: string;
+    /** Historical weather data. */
+    readings: Reading[];
+}
+
+/** A single reading from the weather sensor. */
+export interface Reading {
+    /** Unix timestamp of the reading. */
+    sample_time: number;
+    /** Temperature in degrees Celsius. */
+    temp_c: number;
+    /** Relative humidity in percent. */
+    rh_pct: number;
+    /** Unix timestamp when the server received the reading. */
+    ingest_time: number;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/weather_server/typescript/app/graph.ts	Sat Oct 12 14:03:52 2019 -0400
@@ -0,0 +1,156 @@
+import { WeatherResponse } from './apis';
+import { cToF, dewPointC } from './math';
+
+const HISTORY_SECONDS = 86400;
+
+/**
+ * Sets up everything.
+ * @param tempElement The element where temperature data is.
+ * @param dewPointElement The element where the dew point is.
+ */
+export async function setUp(
+    tempElement: HTMLElement, dewPointElement: HTMLElement) {
+    const nowTS = new Date().getTime() / 1000;
+    const startTS = nowTS - HISTORY_SECONDS;
+    const query = new URL(location.href);
+    query.pathname = query.pathname + '/recent';
+    query.search = '';
+    query.searchParams.set('seconds', String(HISTORY_SECONDS));
+    const results = await fetch(query.href);
+    if (!results.ok) return;
+    const data: WeatherResponse = await results.json();
+    const readings = data.readings;
+    if (readings.length === 0) return;
+
+    const tempsF =
+        readings.map(s => [s.sample_time, cToF(s.temp_c)] as Point);
+    const dewPointsF = readings.map(
+        s => [s.sample_time, cToF(dewPointC(s.temp_c, s.rh_pct))] as Point);
+    setUpElement(tempElement, [startTS, nowTS], tempsF);
+    setUpElement(dewPointElement, [startTS, nowTS], dewPointsF);
+}
+
+type Point = [number, number];
+
+/**
+ * Sets up charting for this element.
+ * @param element The element to put a graph in.
+ * @param timeRange The `[start, end]` of the time range.
+ * @param data The data to chart.
+ */
+function setUpElement(
+    element: HTMLElement, timeRange: [number, number], data: Point[]) {
+    element.insertBefore(document.createElement('canvas'), element.firstChild);
+    element.classList.remove('plain');
+    element.classList.add('fancy');
+    const doDraw = () => redrawCanvas(element, data, timeRange);
+    doDraw();
+    addEventListener('resize', doDraw);
+}
+
+/**
+ *
+ * @param element The parent element to put the `<canvas>` in.
+ * @param data The data to chart.
+ * @param xRange The `[start, end]` of the X range to plot.
+ */
+function redrawCanvas(
+    element: HTMLElement, data: Point[], xRange: [number, number]) {
+    let canvas = element.getElementsByTagName('canvas')[0];
+    if (!canvas) {
+        canvas = document.createElement('canvas');
+        element.insertBefore(canvas, element.firstChild);
+    }
+    const dpr = window.devicePixelRatio || 1;
+    const cssSize = element.getBoundingClientRect();
+    const pxSize: [number, number] =
+        [cssSize.width * dpr, cssSize.height * dpr];
+    canvas.width = pxSize[0];
+    canvas.height = pxSize[1];
+    const ctx = canvas.getContext('2d')!;
+    ctx.clearRect(0, 0, pxSize[0], pxSize[1]);
+    const computed = getComputedStyle(element);
+    ctx.strokeStyle = computed.color || 'white';
+    drawChart(ctx, data, xRange, pxSize);
+}
+
+/** The height of the chart in degrees. */
+const CHART_RANGE_DEGREES = 15;
+
+/**
+ * Charts some data.
+ *
+ * @param ctx The context to draw in.
+ * @param data The data to chart, as `[x, y]` pairs.
+ * @param xRange The bounds of the X axis to draw.
+ * @param size The `[width, height]` of the context.
+ */
+function drawChart(
+    ctx: CanvasRenderingContext2D,
+    data: Point[],
+    xRange: [number, number],
+    size: Point) {
+    const yRange = calculateYRange(data.map(d => d[1]));
+
+    ctx.lineWidth = 1.5;
+
+    ctx.beginPath();
+    const first = project(data[0], size, xRange, yRange);
+    ctx.moveTo(...first);
+    for (const pt of data) {
+        const projected = project(pt, size, xRange, yRange);
+        ctx.lineTo(...projected);
+    }
+    ctx.stroke();
+}
+
+/**
+ * Determines what the Y range of the chart should be.
+ * @param ys The Y values of the chart.
+ * @return The lowest and highest values of the range.
+ */
+function calculateYRange(ys: number[]): [number, number] {
+    const yMax = Math.max(...ys);
+    const yMin = Math.min(...ys);
+    const yMid = (yMin + yMax) / 2;
+    const lastY = ys[ys.length - 1];
+
+    // We want the last value to be, at most, at the top or bottom 1/4 line
+    // of the chart.
+
+    // If the middle of the range is already close enough, just use that.
+    if (CHART_RANGE_DEGREES / 4 <= Math.abs(yMid - lastY)) {
+        return [yMid - CHART_RANGE_DEGREES / 2, yMid + CHART_RANGE_DEGREES / 2];
+    }
+    // Otherwise, clamp the chart range.
+    if (lastY < yMid) {
+        return [lastY - CHART_RANGE_DEGREES / 4, lastY + 3 * CHART_RANGE_DEGREES / 4];
+    }
+    return [lastY - 3 * CHART_RANGE_DEGREES / 4, lastY + CHART_RANGE_DEGREES / 4];
+}
+
+/**
+ * Projects a Cartesian coordinate into Canvas space.
+ *
+ * @param coord The `[x, y]` coordinate to project.
+ * @param size The `[width, height]` of the context.
+ * @param xRange The range of X values in the context.
+ * @param yRange The range of Y values in the context.
+ * @return The `[x, y]` coordinate in Canvas space.
+ */
+function project(
+    coord: Point,
+    size: Point,
+    xRange: [number, number],
+    yRange: [number, number]): Point {
+    const [x, y] = coord;
+    const [xMin, xMax] = xRange;
+    const xSpan = xMax - xMin;
+    const [yMin, yMax] = yRange;
+    const ySpan = yMax - yMin;
+    const [xSize, ySize] = size;
+    return [
+        (x - xMin) / xSpan * xSize,
+        (yMax - y) / ySpan * ySize,
+    ]
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/weather_server/typescript/app/math.ts	Sat Oct 12 14:03:52 2019 -0400
@@ -0,0 +1,18 @@
+/** Converts Celsius to Fahrenheit. */
+export function cToF(tempC: number): number {
+    return tempC * 9 / 5 + 32;
+}
+
+const MAGNUS_B = 17.62;
+const MAGNUS_C = 243.12;
+
+/** The gamma function to calculate dew point. */
+function gammaFn(tempC: number, rhPct: number): number {
+    return Math.log(rhPct / 100) + MAGNUS_B * tempC / (MAGNUS_C + tempC);
+}
+
+/** Calculates the dew point. */
+export function dewPointC(tempC: any, rhPct: any): number {
+    const gamma = gammaFn(tempC, rhPct);
+    return MAGNUS_C * gamma / (MAGNUS_B - gamma);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/weather_server/typescript/app/tsconfig.json	Sat Oct 12 14:03:52 2019 -0400
@@ -0,0 +1,61 @@
+{
+  "files": ["graph.ts"],
+  "compilerOptions": {
+    /* Basic Options */
+    "target": "es6",                          /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
+    "module": "amd",                     /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
+    // "lib": [],                             /* Specify library files to be included in the compilation. */
+    // "allowJs": true,                       /* Allow javascript files to be compiled. */
+    // "checkJs": true,                       /* Report errors in .js files. */
+    // "jsx": "preserve",                     /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
+    // "declaration": true,                   /* Generates corresponding '.d.ts' file. */
+    // "declarationMap": true,                /* Generates a sourcemap for each corresponding '.d.ts' file. */
+    "sourceMap": true,                     /* Generates corresponding '.map' file. */
+    "outFile": "../../static/graph.js",                       /* Concatenate and emit output to single file. */
+    // "outDir": "./",                        /* Redirect output structure to the directory. */
+    // "rootDir": "./",                       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
+    // "composite": true,                     /* Enable project compilation */
+    // "removeComments": true,                /* Do not emit comments to output. */
+    // "noEmit": true,                        /* Do not emit outputs. */
+    // "importHelpers": true,                 /* Import emit helpers from 'tslib'. */
+    // "downlevelIteration": true,            /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
+    // "isolatedModules": true,               /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
+
+    /* Strict Type-Checking Options */
+    "strict": true,                           /* Enable all strict type-checking options. */
+    // "noImplicitAny": true,                 /* Raise error on expressions and declarations with an implied 'any' type. */
+    // "strictNullChecks": true,              /* Enable strict null checks. */
+    // "strictFunctionTypes": true,           /* Enable strict checking of function types. */
+    // "strictBindCallApply": true,           /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
+    // "strictPropertyInitialization": true,  /* Enable strict checking of property initialization in classes. */
+    // "noImplicitThis": true,                /* Raise error on 'this' expressions with an implied 'any' type. */
+    // "alwaysStrict": true,                  /* Parse in strict mode and emit "use strict" for each source file. */
+
+    /* Additional Checks */
+    "noUnusedLocals": true,                /* Report errors on unused locals. */
+    "noUnusedParameters": true,            /* Report errors on unused parameters. */
+    "noImplicitReturns": true,             /* Report error when not all code paths in function return a value. */
+    "noFallthroughCasesInSwitch": true,    /* Report errors for fallthrough cases in switch statement. */
+
+    /* Module Resolution Options */
+    // "moduleResolution": "node",            /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
+    // "baseUrl": "./",                       /* Base directory to resolve non-absolute module names. */
+    // "paths": {},                           /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
+    // "rootDirs": [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */
+    // "typeRoots": [],                       /* List of folders to include type definitions from. */
+    // "types": [],                           /* Type declaration files to be included in compilation. */
+    // "allowSyntheticDefaultImports": true,  /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
+    "esModuleInterop": true,                  /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
+    // "preserveSymlinks": true,              /* Do not resolve the real path of symlinks. */
+
+    /* Source Map Options */
+    // "sourceRoot": "",                      /* Specify the location where debugger should locate TypeScript files instead of source locations. */
+    // "mapRoot": "",                         /* Specify the location where debugger should locate map files instead of generated locations. */
+    // "inlineSourceMap": true,               /* Emit a single file with source maps instead of having a separate file. */
+    "inlineSources": true                     /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
+
+    /* Experimental Options */
+    // "experimentalDecorators": true,        /* Enables experimental support for ES7 decorators. */
+    // "emitDecoratorMetadata": true,         /* Enables experimental support for emitting type metadata for decorators. */
+  }
+}