Skip to content

Commit

Permalink
Weather Alert Filters (#81)
Browse files Browse the repository at this point in the history
Added weather alert filters.
  • Loading branch information
kshetline authored Jun 12, 2022
1 parent 1122b9b commit d42f810
Show file tree
Hide file tree
Showing 26 changed files with 431 additions and 78 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 3.3.0

* Added weather alert filters.

## 3.2.4

* Fixed iOS Safari layout bug. This bug might have affected the layout of the planet symbols on other displays.
Expand Down
69 changes: 41 additions & 28 deletions README.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -643,7 +643,7 @@ function portValidate(s: string): boolean {
}

const DOMAIN_PATTERN =
/^(((?!-))(xn--|_)?[-a-z0-9]{0,61}[a-z0-9]\.)*(xn--)?([a-z0-9][-a-z0-9]{0,60}|[-a-z0-9]{1,30}\.[a-z]{2,})(:\d{1,5})?$/i;
/^(((?!-))(xn--|_)?[-a-z\d]{0,61}[a-z\d]\.)*(xn--)?([a-z\d][-a-z\d]{0,60}|[-a-z\d]{1,30}\.[a-z]{2,})(:\d{1,5})?$/i;

function ntpValidate(s: string): boolean {
const domains = s.split(',').map(d => d.trim());
Expand Down
Binary file modified img/awc-dlog-alarms.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/awc-dlog-alert-filters.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified img/awc-dlog-locations.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified img/awc-dlog-options.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "aw-clock",
"version": "3.2.4",
"version": "3.3.0",
"license": "MIT",
"author": "Kerry Shetline <kerry@shetline.com>",
"scripts": {
Expand Down
4 changes: 2 additions & 2 deletions sass/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion sass/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "aw-clock-sass",
"version": "3.2.4",
"version": "3.3.0",
"description": "SASS builder for aw-clock",
"keywords": [
"sass"
Expand Down
4 changes: 2 additions & 2 deletions server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "aw-clock-server",
"version": "3.2.4",
"version": "3.3.0",
"license": "MIT",
"author": "Kerry Shetline <kerry@shetline.com>",
"private": true,
Expand Down
4 changes: 2 additions & 2 deletions server/src/process-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,14 @@ const MAX_MARK_TIME_DELAY = 100;
const NO_OP = (): void => {};

export function stripFormatting(s: string): string {
return s?.replace(/\x1B\[[0-9;]*[A-Za-z]/g, '');
return s?.replace(/\x1B\[[\d;]*[A-Za-z]/g, '');
}

function errorish(s: string): boolean {
s = stripFormatting(s);

return /\b(failed|exception|invalid|operation not permitted|isn't a valid|Cannot resolve|must be specified|must implement|need to install|doesn't exist|are required|should be strings?)\b/i.test(s) ||
/[_0-9a-z](Error|Exception|Invalid)\b/.test(s) || /\[ERR_|code: 'ERR/.test(s);
/[_\da-z](Error|Exception|Invalid)\b/.test(s) || /\[ERR_|code: 'ERR/.test(s);
}

export function spawn(command: string, args: string[], options?: any): ChildProcess;
Expand Down
2 changes: 1 addition & 1 deletion server/src/request-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const REQUEST_TIMEOUT = 55000; // 55 seconds
const REQUEST_TIMEOUT_CHECK = 90; // seconds

function filterUrl(url: string): string {
return url.replace(/(?<=\?key=)\w+(?=&)/, '...').replace(/(?<=\/forecast\/)[0-9A-F]+(?=\/)/i, '...')
return url.replace(/(?<=\?key=)\w+(?=&)/, '...').replace(/(?<=\/forecast\/)[\dA-F]+(?=\/)/i, '...')
.replace(/(?<=&id=)\w+(?=[& ])/, '...');
}

Expand Down
2 changes: 1 addition & 1 deletion server/src/shared-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export const AlertKeys = ['description', 'expires', 'severity', 'title', 'url'];
export interface Alert {
description: string;
expires: number; // See CommonConditions
severity: 'advisory' | 'watch' | 'warning';
severity: 'info' | 'advisory' | 'watch' | 'warning';
time: number; // See CommonConditions
title: string;
url?: string;
Expand Down
3 changes: 2 additions & 1 deletion src/app.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CurrentTemperatureHumidity, TimeFormat } from './shared-types';
import { Settings } from './settings';
import { AlertFilter, Settings } from './settings';
import { AwcDefaults, TimeInfo } from '../server/src/shared-types';
import { Timezone } from '@tubular/time';

Expand All @@ -8,6 +8,7 @@ export interface AppService {
getAlarmTime(): number;
getApiServer(): string;
getCurrentTime(bias?: number): number;
getAlertFilters(): AlertFilter[];
getIndoorOption(): string;
getLatestDefaults(): AwcDefaults;
getOutdoorOption(): string;
Expand Down
10 changes: 10 additions & 0 deletions src/assets/filter.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 5 additions & 5 deletions src/clock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export class Clock {
private inMinuteOfLeapSecond = false;
private pendingLeapSecondForMonth = 0;
private firstLeapSecondPoll = true;
private lastLeapSecondCheckDay = -1;
private lastLeapSecondCheckHour = -1;
private upcomingLeapSecond: CurrentDelta;
private dut1PositionAdjustmentNeeded = true;

Expand Down Expand Up @@ -341,8 +341,8 @@ export class Clock {
const leapSecondForMonth = (minuteOfLeapSecond && timeInfo.leapSecond) ||
this.checkPendingLeapSecondForMonth(wallTimeUtc);

if (this.lastLeapSecondCheckDay !== wallTimeUtc.d) {
this.lastLeapSecondCheckDay = wallTimeUtc.d;
if (this.lastLeapSecondCheckHour !== wallTimeUtc.hour) {
this.lastLeapSecondCheckHour = wallTimeUtc.hour;
this.getLeapSecondInfo();
this.adjustTimeFontSize();
}
Expand Down Expand Up @@ -529,12 +529,12 @@ export class Clock {
this.upcomingLeapSecond = data;

if (data.delta === 0 || !data.dut1)
this.lastLeapSecondCheckDay = -1;
this.lastLeapSecondCheckHour = -1;
}
catch {
setTimeout(() => {
this.upcomingLeapSecond = undefined;
this.lastLeapSecondCheckDay = -1;
this.lastLeapSecondCheckHour = -1;
}, LEAP_SECOND_RETRY_DELAY);
}
// Randomly delay polling so that multiple clock instances don't all poll at the same time every day.
Expand Down
81 changes: 63 additions & 18 deletions src/forecast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@ import $ from 'jquery';
import { DateTime, Timezone } from '@tubular/time';
import { abs, cos_deg, floor, max, min, round, sign, sin_deg } from '@tubular/math';
import {
blendColors, doesCharacterGlyphExist, getTextWidth, htmlEscape, isChrome, isChromium, isEdge, isObject,
last, processMillis, toNumber
blendColors, clone, doesCharacterGlyphExist, getTextWidth, htmlEscape, isChrome, isChromium, isEdge, isObject, last,
processMillis, toNumber
} from '@tubular/util';
import { CurrentConditions, ForecastData, HourlyConditions } from '../server/src/shared-types';
import { Alert, CurrentConditions, ForecastData, HourlyConditions } from '../server/src/shared-types';
import { reflow } from './svg-flow';
import {
ClickishEvent,
compassPoint, convertPressure, convertSpeed, convertTemp, describeArc, displayHtml, formatHour,
getJson, JsonOptions, kphToKnots, localDateString, mphToKnots, setSvgHref, stopPropagation
ClickishEvent, compassPoint, convertPressure, convertSpeed, convertTemp, describeArc, displayHtml, formatHour, getJson,
JsonOptions, kphToKnots, localDateString, mphToKnots, setSvgHref, stopPropagation
} from './awc-util';
import { windBarbsSvg } from './wind-barbs';
import { CurrentTemperatureHumidity, HourlyForecast, TimeFormat } from './shared-types';
import { AlertFilterType } from './settings';

interface SVGAnimationElementPlus extends SVGAnimationElement {
beginElement: () => void;
Expand Down Expand Up @@ -88,6 +88,13 @@ function eventInside(event: MouseEvent | Touch, elem: HTMLElement): boolean {
return rect.x <= x && x <= rect.right && rect.y <= y && y <= rect.bottom;
}

function concatenateAlerts(alerts: string[]): string {
return alerts.map(a => a.replace(/\r\n|\r/g, '\n').trim()
.replace(/\s[\s\x23-\x2F\x3A-\x40]+$/, '') // Remove seemingly random trailing characters from alerts.
.replace(/^\* /gm, '• ') // Replace asterisks used as bullets with real bullets.
).join(BULLET_SPACER);
}

export class Forecast {
private readonly currentIcon: JQuery;
private readonly marqueeOuterWrapper: JQuery;
Expand Down Expand Up @@ -1005,6 +1012,8 @@ export class Forecast {
let newText: string;
let maxSeverity = 0;
const alerts: string[] = [];
let droppedAlertSymbols = ' ';
const droppedAlerts: string[] = [];
const now = this.appService.getCurrentTime();

if (this.appService.sensorDeadAir())
Expand All @@ -1015,21 +1024,28 @@ export class Forecast {

if (forecastData?.alerts) {
forecastData.alerts.forEach(alert => {
alert = clone(alert);

const expires = alert.expires * 1000;

if (expires >= now) {
const severities = ['advisory', 'watch', 'warning'];
maxSeverity = max(severities.indexOf(alert.severity) + 1, maxSeverity);
alerts.push(alert.title + ': ' + alert.description);
const allowed = this.alertAllowed(alert);
const severity = severities.indexOf(alert.severity) + 1;

if (allowed) {
maxSeverity = max(severity, maxSeverity);
alerts.push(alert.title + ': ' + alert.description);
}
else {
droppedAlertSymbols += ['🔵', '🟠', '🔴'][severity - 1] || '';
droppedAlerts.push(alert.title + ': ' + alert.description);
}
}
});
}

const alertText = alerts.map(a => a.replace(/\r\n|\r/g, '\n').trim()
.replace(/\s[\s\x23-\x2F\x3A-\x40]+$/, '') // Remove seemingly random trailing characters from alerts.
.replace(/^\* /gm, '• ') // Replace asterisks used as bullets with real bullets.
).join(BULLET_SPACER);

const alertText = concatenateAlerts(alerts) + droppedAlertSymbols.trimEnd();
let background: string;
let color: string;

Expand Down Expand Up @@ -1071,7 +1087,33 @@ export class Forecast {
this.marqueeWrapper.css('background-color', background);
this.marqueeOuterWrapper.css('color', color);
this.marqueeWrapper.css('color', color);
this.updateMarqueeAnimation(newText);
this.updateMarqueeAnimation(newText, concatenateAlerts(droppedAlerts));
}

private alertAllowed(alert: Alert): boolean {
const title = alert.title.toLowerCase();
const fullText = title + '\n' + alert.description.toLowerCase().replace(/\s+/g, ' ').trim();
const filters = this.appService.getAlertFilters();

return filters.findIndex(filter => {
const text = (filter.checkDescription ? fullText : title);
const $ = /^\/([^/]+)\/(u?)$/.exec(filter.content.trim());
let matched: boolean;

if ($)
matched = new RegExp($[1], 'i' + $[2]).test(text);
else
matched = text.includes(filter.content.toLowerCase());

if (matched) {
if (filter.type === AlertFilterType.HIDE)
return true;
else
alert.severity = 'info';
}

return false;
}) < 0;
}

private checkAspectRatio(): void {
Expand All @@ -1095,7 +1137,7 @@ export class Forecast {
}
}

private updateMarqueeAnimation(newText: string): void {
private updateMarqueeAnimation(newText: string, droppedAlerts?: string): void {
if (newText !== null) {
if (newText === this.marqueeText)
return;
Expand All @@ -1112,12 +1154,15 @@ export class Forecast {
this.marquee.css('width', marqueeWidth + 'px');
this.marquee.css('text-indent', '0');

// Try to undo hard word-wrap (too bad lookbehinds aren't reliably supported yet in web browsers).
this.marqueeDialogText = newText.replace(BULLET_REGEX, '\n<hr>').replace(/([-0-9a-z,])\n(?=[a-z]|(\d[^.#*)\]]))/gi, '$1 ')
// Try to undo hard word-wrap.
this.marqueeDialogText = (newText + (droppedAlerts ? BULLET_SPACER + droppedAlerts : ''))
.replace(BULLET_REGEX, '\n<hr>').replace(/([-\da-z,])\n(?=[a-z]|(\d[^.#*)\]]))/gi, '$1 ')
// No more than one blank line, and no trailing blank lines.
.replace(/\n{3,}/g, '\n\n').trim().replace(/\n/g, '<br>\n')
// Improve alert formatting.
.replace(SUBJECT_INTRO_PATTERN, '$1: ');
.replace(SUBJECT_INTRO_PATTERN, '$1: ')
// Remove hidden alert icons.
.replace(/ (\uD83D[\uDD34-\uDD35\uDFe0])+/, '');

if (textWidth <= marqueeWidth) {
this.marquee.html(newText);
Expand Down
11 changes: 9 additions & 2 deletions src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -431,11 +431,12 @@
<li>Options</li>
<li>Location Search</li>
<li>Alarms</li>
<li>Alert Filters</li>
<li>Update</li>
</ul>
<span style="white-space: nowrap;">
<span class="keyboard-option">
<input type="checkbox" id="onscreen-kb" name="onscreen-kb">
<label for="onscreen-kb">Onscreen keyboard</label>
<label for="onscreen-kb">Onscreen<br>keyboard</label>
</span>
</div>
<div class="tab-wrapper">
Expand Down Expand Up @@ -634,6 +635,12 @@
</div>
</div>
</div>
<div class="tab-panel alert-section">
<button id="add-filter"><img src="assets/filter.svg" width="16" height="16" alt="filter"> Add Filter</button>
<div id="filter-list-wrapper">
<div id="filter-list"></div>
</div>
</div>
<div class="tab-panel update-section">
<div class="update-header">
<span>Latest version: <span class="latest-version-text"></span></span>
Expand Down
6 changes: 5 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { DateTime, Timezone, parseISODateTime, pollForTimezoneUpdates, zonePolle
import { abs, ceil, floor, irandom, max, min, sqrt } from '@tubular/math';
import { eventToKey, isBoolean, isEffectivelyFullScreen, isEqual, isFirefox, isObject, setFullScreen } from '@tubular/util';
import { Sensors } from './sensors';
import { allowAdminFeatures, apiServer, localServer, runningDev, Settings } from './settings';
import { AlertFilter, allowAdminFeatures, apiServer, localServer, runningDev, Settings } from './settings';
import { SettingsDialog } from './settings-dialog';
import { AwcDefaults, TimeInfo } from '../server/src/shared-types';
import { reflow, updateSvgFlowItems } from './svg-flow';
Expand Down Expand Up @@ -573,6 +573,10 @@ class AwClockApp implements AppService {
this.clock.hasCompletingAnimation = isScrolling;
}

getAlertFilters(): AlertFilter[] {
return this.settings.alertFilters || [];
}

getIndoorOption(): string {
return this.settings.indoorOption;
}
Expand Down
Loading

0 comments on commit d42f810

Please sign in to comment.