tilfluktsrom/pwa/src/ui/compass-view.ts
Ole-Morten Duesund e8428de775 Add progressive web app companion for cross-platform access
Vite + TypeScript PWA that mirrors the Android app's core features:
- Pre-processed shelter data (build-time UTM33N→WGS84 conversion)
- Leaflet map with shelter markers, user location, and offline tiles
- Canvas compass arrow (ported from DirectionArrowView.kt)
- IndexedDB shelter cache with 7-day staleness check
- Service worker with CacheFirst tiles and precached app shell
- i18n for en, nb, nn (ported from Android strings.xml)
- iOS/Android compass handling with low-pass filter
- Respects user map interaction (no auto-snap on pan/zoom)
- Build revision cache-breaker for reliable SW updates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 17:41:38 +01:00

89 lines
2.5 KiB
TypeScript

/**
* Canvas-based direction arrow pointing toward the selected shelter.
* Ported from DirectionArrowView.kt — same 7-point arrow polygon.
*
* Arrow rotation = shelterBearing - deviceHeading
*/
const ARROW_COLOR = '#FF6B35';
const OUTLINE_COLOR = '#FFFFFF';
const OUTLINE_WIDTH = 4;
let canvas: HTMLCanvasElement | null = null;
let ctx: CanvasRenderingContext2D | null = null;
let currentAngle = 0;
let animFrameId = 0;
/** Initialize the compass canvas inside the given container element. */
export function initCompass(container: HTMLElement): void {
canvas = document.createElement('canvas');
canvas.id = 'compass-canvas';
canvas.style.width = '100%';
canvas.style.height = '100%';
container.prepend(canvas);
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
}
function resizeCanvas(): void {
if (!canvas) return;
const rect = canvas.parentElement!.getBoundingClientRect();
canvas.width = rect.width * devicePixelRatio;
canvas.height = rect.height * devicePixelRatio;
draw();
}
/**
* Set the arrow direction in degrees.
* 0 = pointing up, positive = clockwise.
*/
export function setDirection(degrees: number): void {
currentAngle = degrees;
if (animFrameId) cancelAnimationFrame(animFrameId);
animFrameId = requestAnimationFrame(draw);
}
function draw(): void {
if (!canvas) return;
ctx = canvas.getContext('2d');
if (!ctx) return;
const w = canvas.width;
const h = canvas.height;
const cx = w / 2;
const cy = h / 2;
const size = Math.min(w, h) * 0.4;
ctx.clearRect(0, 0, w, h);
ctx.save();
ctx.translate(cx, cy);
ctx.rotate((currentAngle * Math.PI) / 180);
// 7-point arrow polygon (same geometry as DirectionArrowView.kt)
ctx.beginPath();
ctx.moveTo(0, -size); // tip
ctx.lineTo(size * 0.5, size * 0.3); // right wing
ctx.lineTo(size * 0.15, size * 0.1); // right notch
ctx.lineTo(size * 0.15, size * 0.7); // right tail
ctx.lineTo(-size * 0.15, size * 0.7); // left tail
ctx.lineTo(-size * 0.15, size * 0.1); // left notch
ctx.lineTo(-size * 0.5, size * 0.3); // left wing
ctx.closePath();
ctx.fillStyle = ARROW_COLOR;
ctx.fill();
ctx.strokeStyle = OUTLINE_COLOR;
ctx.lineWidth = OUTLINE_WIDTH;
ctx.stroke();
ctx.restore();
}
/** Clean up compass resources. */
export function destroyCompass(): void {
window.removeEventListener('resize', resizeCanvas);
if (animFrameId) cancelAnimationFrame(animFrameId);
canvas?.remove();
canvas = null;
ctx = null;
}