Mercurial > personal > weather-server
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. */ + } +}