Skip to content

Commit 5b3f42e

Browse files
authored
Merge pull request #3 from cs-util-com/codex/implement-application-as-per-readme.md
Implement Snap2Map calibration pipeline and UI
2 parents d83d9c9 + f6a4935 commit 5b3f42e

19 files changed

+2291
-372
lines changed

README.md

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,20 +43,23 @@
4343
### 3.1 First run
4444

4545
1. **Map Manager (empty state)** → short tutorial → **Import photo** (camera or gallery).
46-
2. Enter **Pair Mode** to add reference pairs.
46+
2. Import automatically launches a **guided Pair Mode** session (toasts instruct each step) so the user can capture the minimum viable set of reference pairs.
4747
3. After ≥2 pairs, **Live** becomes available.
4848

4949
### 3.2 Pair Mode (add/edit) — explicit mode
5050

51-
* Enter via **FAB “Add pair”** or long-press on an existing marker.
52-
* **Top status bar** (highlighted): “Pair #n – Step 1/2” (color frame in mode).
53-
* **Bottom tabs**: **Photo****OSM**.
51+
* Enter via **FAB “Add pair”**, long-press on an existing marker, or automatically right after importing a map photo.
52+
* **Top status bar** (highlighted): “Pair #n – Step 1/2” (color frame in mode). Guided mode also shows progress pills (“Photo point”, “Map point”) that swap as each step completes.
53+
* **Bottom tabs**: **Photo****OSM**. During guided mode the active tab auto-switches after each placement to keep the workflow hands-free.
5454
* Flow (active slot):
5555

56-
* Tap either **Photo** (place pixel pin with loupe) **or** **OSM** (place world pin via map tap or **Use my position**).
57-
* Switch to the other tab and place the missing pin → pair completes.
56+
* Guided mode opens on **Photo** immediately after import and displays a toast: “Tap the first point on your photo.”
57+
* Once the pixel pin is dropped, the UI auto-switches to **OSM**, updates the toast (“Now tap the matching spot on the map”), and enables “Use my position” as a contextual hint.
58+
* Manual flow (when initiated later) still allows starting on either side.
59+
* After the matching map pin is placed, the pair preview appears. Guided mode automatically toggles back to the **Photo** tab, advances to the next pair, and repeats the instruction cycle until the minimum required pair count (2) is collected. After that the flow switches back to manual control while encouraging the user to add more pairs for accuracy.
5860
* **Drag & drop** on both sides updates pair live.
5961
* **Confirm (✓)** applies; **Cancel (←)** discards changes to the active pair only.
62+
* Guided mode auto-confirms each pair as soon as both pins are placed, surfaces a toast summarizing the residual (“Pair saved — residual 12 m”), and exits once two pairs exist (with a prompt encouraging more for higher accuracy).
6063
* **Outliers** shown **red**, inliers **green**; residual (m) visible in list and as label on pins.
6164
* Edit existing pair later via long-press or from the pairs list (secondary screen).
6265

@@ -251,7 +254,7 @@ export interface PositionProjector {
251254

252255
* **Photo** (ImageOverlay, anchor pins, heatmap toggle, accuracy ring)
253256
* **OSM** (standard tiles, user dot, can place map pins)
254-
* **Pair Mode** (status bar + amber frame; ✓ Confirm / ← Cancel)
257+
* **Pair Mode** (status bar + amber frame; ✓ Confirm / ← Cancel; guided variant adds instruction toasts and auto-tab switching)
255258
* **Settings/Info** (language: English; tile URL override; export/import; reset)
256259
* **Anchors**
257260

@@ -266,6 +269,7 @@ export interface PositionProjector {
266269
* **Toasts/banners**
267270

268271
* Version update ready (auto on next open).
272+
* Guided pairing script: “Tap the first point on your photo”, “Now tap the matching spot on the map”, “Pair saved — residual ··m. Add another for better accuracy.”
269273
* Prompts: add more points / refine when thresholds exceeded.
270274
* Errors: storage full, file too big, permissions denied.
271275

config/babel.config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
module.exports = {
2-
presets: [['@babel/preset-env', {targets: {node: 'current'}}]],
2+
sourceType: 'module',
3+
presets: [['@babel/preset-env', { targets: { node: 'current' } }]],
34
};

config/jest.config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ module.exports = {
1212
'!src/**/*.property.test.js', // exclude property-based tests
1313
'!src/**/__mocks__/**' // exclude any mocks if added later
1414
],
15+
transform: {
16+
// Use a double-escaped dot so the regex sees a literal dot and ESLint doesn't flag a useless escape
17+
'^.+\\.js$': ['babel-jest', { configFile: require.resolve('./babel.config.js') }],
18+
},
1519
coverageReporters: ["json", "lcov", "text", "clover"],
1620
coverageThreshold: {
1721
global: {

index.html

Lines changed: 85 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,134 +1,104 @@
1-
<!-- For the user stories & requirements see the README.md file -->
2-
31
<!DOCTYPE html>
42
<html lang="en">
53
<head>
64
<meta charset="UTF-8">
75
<meta name="viewport" content="width=device-width, initial-scale=1.0">
8-
<title>Decoupled Modules Example with Person Class</title>
9-
<!-- Favicon: A white box --> <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' fill='%23fff'/%3E%3C/svg%3E" type="image/svg+xml">
10-
<!-- Use Tailwind CSS for all CSS styling -->
6+
<title>Snap2Map</title>
7+
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' rx='18' fill='%233256d1'/%3E%3Cpath d='M25 65 L45 40 L60 55 L75 35' stroke='%23fff' stroke-width='8' fill='none' stroke-linecap='round'/%3E%3Ccircle cx='75' cy='35' r='6' fill='%23fff'/%3E%3C/svg%3E" type="image/svg+xml">
8+
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" integrity="sha384-sHL9NAb7lN7rfvG5lfHpm643Xkcjzp4jFvuavGOndn6pjVqS6ny56CAt3nsEVT4H" crossorigin="anonymous">
119
<script src="https://cdn.tailwindcss.com"></script>
12-
<style>
13-
@view-transition {
14-
navigation: auto;
15-
}
16-
17-
nav a[aria-current="page"]:after {
18-
border-color: white;
19-
view-transition-name: posts-nav;
20-
}
21-
22-
/* Old stuff going out */
23-
::view-transition-old(posts-nav) {
24-
animation: fade 0.2s linear forwards;
25-
height: 100%;
26-
}
27-
28-
/* New stuff coming in */
29-
::view-transition-new(posts-nav) {
30-
animation: fade 0.3s linear reverse;
31-
height: 100%;
32-
}
33-
34-
@keyframes fade {
35-
from { opacity: 1; }
36-
to { opacity: 0; }
37-
}
38-
</style>
39-
<script>
40-
// Only use if browser supports View Transitions
41-
if (document.startViewTransition) {
42-
window.addEventListener('click', (e) => {
43-
// Only handle internal links
44-
if (e.target.tagName === 'A' && e.target.origin === window.location.origin) {
45-
e.preventDefault();
46-
document.startViewTransition(() => {
47-
window.location.href = e.target.href;
48-
});
49-
}
50-
});
51-
}
52-
</script>
53-
<script src="src/index.js"></script>
5410
</head>
55-
<body class="font-mono p-8 bg-gray-50">
56-
<div class="max-w-4xl mx-auto">
57-
<header class="mb-8">
58-
<h1 class="text-3xl font-bold text-blue-600 mb-4">Template Web App</h1>
59-
<nav class="bg-blue-600 text-white p-4 rounded-lg mb-4">
60-
<a href="index.html" class="mr-6 hover:underline font-bold relative" aria-current="page">
61-
Home
62-
<span class="absolute bottom-0 left-0 w-full h-0.5 bg-white"></span>
63-
</a>
64-
<a href="pages/features.html" class="mr-6 hover:underline">Features</a>
65-
<a href="pages/about.html" class="hover:underline">About</a>
66-
</nav>
67-
<div class="bg-blue-100 border-l-4 border-blue-500 p-4 mb-6">
68-
<p class="text-sm text-blue-700">This is a Tailwind CSS demonstration with modular JavaScript.</p>
69-
</div>
11+
<body class="bg-slate-950 text-slate-100 min-h-screen">
12+
<div class="max-w-6xl mx-auto px-4 py-10 space-y-8">
13+
<header class="space-y-4">
14+
<h1 class="text-4xl font-black tracking-tight">Snap2Map</h1>
15+
<p class="text-slate-300 max-w-3xl">
16+
Photograph any trailboard or printed map, drop a few reference pairs, and watch your live GPS position glide across the photo. Works offline, with OpenStreetMap context when you are connected.
17+
</p>
7018
</header>
71-
72-
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
73-
<!-- Card 1 -->
74-
<div class="bg-white p-6 rounded-lg shadow-md">
75-
<h2 class="text-xl font-bold mb-4 text-gray-800">Greeting Card</h2>
76-
<p class="text-gray-600 mb-4">Click the button below to greet the person.</p>
77-
<button id="greetBtn" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline transition duration-300">
78-
Greet
79-
</button>
19+
20+
<section class="bg-slate-900/70 border border-slate-700 rounded-xl p-6 space-y-4">
21+
<h2 class="text-xl font-semibold text-slate-100">1. Import map photo</h2>
22+
<p class="text-sm text-slate-300">Use a sharp, well-lit image. Snap2Map keeps only an optimized copy on device.</p>
23+
<label class="block w-full">
24+
<span class="sr-only">Upload map image</span>
25+
<input id="mapImageInput" type="file" accept="image/*" capture="environment" class="block w-full text-sm text-slate-200 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-blue-600 file:text-white hover:file:bg-blue-500">
26+
</label>
27+
</section>
28+
29+
<section class="bg-slate-900/70 border border-slate-700 rounded-xl p-6 space-y-4">
30+
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
31+
<div>
32+
<h2 class="text-xl font-semibold text-slate-100">2. Create reference pairs</h2>
33+
<p id="pairStatus" class="text-sm text-slate-300">Tap “Start pair” and select a pixel on the photo followed by its real-world location on the map.</p>
34+
</div>
35+
<div class="flex flex-wrap gap-2">
36+
<button id="addPairButton" class="px-4 py-2 rounded-lg bg-blue-600 text-white text-sm font-semibold hover:bg-blue-500 transition">Start pair</button>
37+
<button id="usePositionButton" class="px-4 py-2 rounded-lg bg-emerald-600 text-white text-sm font-semibold hover:bg-emerald-500 transition">Use my position</button>
38+
<button id="confirmPairButton" class="px-4 py-2 rounded-lg bg-violet-600 text-white text-sm font-semibold opacity-80 disabled:opacity-40 disabled:cursor-not-allowed hover:bg-violet-500 transition" disabled>Confirm pair</button>
39+
<button id="cancelPairButton" class="px-4 py-2 rounded-lg bg-slate-700 text-white text-sm font-semibold opacity-80 disabled:opacity-40 disabled:cursor-not-allowed hover:bg-slate-600 transition" disabled>Cancel</button>
40+
</div>
8041
</div>
81-
82-
<!-- Card 2 -->
83-
<div class="bg-white p-6 rounded-lg shadow-md">
84-
<h2 class="text-xl font-bold mb-4 text-gray-800">Tailwind Features</h2>
85-
<ul class="list-disc pl-5 space-y-2 text-gray-600">
86-
<li>Responsive design with <span class="font-bold text-blue-500">breakpoints</span></li>
87-
<li>Utility-first CSS framework</li>
88-
<li>Customizable with configuration</li>
89-
</ul>
90-
<div class="mt-4">
91-
<div class="flex gap-2">
92-
<span class="bg-red-200 text-red-800 text-xs p-1 rounded">Tag</span>
93-
<span class="bg-green-200 text-green-800 text-xs p-1 rounded">Utility</span>
94-
<span class="bg-yellow-200 text-yellow-800 text-xs p-1 rounded">Responsive</span>
42+
43+
<div class="bg-slate-950/60 border border-slate-700 rounded-lg">
44+
<div class="flex">
45+
<button id="photoTabButton" class="flex-1 px-4 py-2 text-sm font-semibold bg-blue-600 text-white rounded-tl-lg">Photo</button>
46+
<button id="osmTabButton" class="flex-1 px-4 py-2 text-sm font-semibold bg-white/10 text-blue-300 rounded-tr-lg">OpenStreetMap</button>
47+
</div>
48+
<div class="p-3">
49+
<div id="photoView" class="rounded-lg overflow-hidden border border-slate-800">
50+
<div id="photoMap" class="h-96"></div>
51+
</div>
52+
<div id="osmView" class="hidden rounded-lg overflow-hidden border border-slate-800">
53+
<div id="osmMap" class="h-96"></div>
9554
</div>
9655
</div>
9756
</div>
98-
</div>
99-
100-
<div class="bg-white p-6 rounded-lg shadow">
101-
<h2 class="text-xl font-bold mb-4 text-gray-800">Interactive Components</h2>
102-
<div class="mb-4">
103-
<label class="block text-gray-700 text-sm font-bold mb-2" for="username">
104-
Username
105-
</label>
106-
<input class="w-full p-2 border rounded text-gray-700 focus:outline-none focus:shadow-outline" id="username" type="text" placeholder="Username">
57+
</section>
58+
59+
<section class="bg-slate-900/70 border border-slate-700 rounded-xl p-6 space-y-4">
60+
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-3">
61+
<div class="flex items-center gap-2 text-sm">
62+
<span id="calibrationBadge" class="px-2 py-1 rounded text-xs font-semibold bg-gray-200 text-gray-700">No calibration</span>
63+
<span id="calibrationStatus" class="text-slate-200">Add at least two reference pairs to calibrate the photo.</span>
64+
</div>
65+
<div class="text-sm text-slate-300" id="residualSummary"></div>
10766
</div>
108-
<div class="flex justify-between">
109-
<button class="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded focus:outline-none transition">
110-
Save
111-
</button>
112-
<button class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded focus:outline-none transition">
113-
Cancel
114-
</button>
67+
<div id="accuracyDetails" class="text-sm text-blue-200"></div>
68+
<div class="text-sm" id="gpsStatus">Import a map photo to get started.</div>
69+
</section>
70+
71+
<section class="bg-slate-900/70 border border-slate-700 rounded-xl p-6">
72+
<h2 class="text-xl font-semibold text-slate-100 mb-4">Reference pairs</h2>
73+
<div class="overflow-x-auto">
74+
<table class="min-w-full text-left" id="pairTable">
75+
<thead class="text-xs uppercase tracking-wider text-slate-400 border-b border-slate-700">
76+
<tr>
77+
<th class="px-3 py-2">Pixel (x, y)</th>
78+
<th class="px-3 py-2">World (lat, lon)</th>
79+
<th class="px-3 py-2">Residual</th>
80+
<th class="px-3 py-2 text-right">Actions</th>
81+
</tr>
82+
</thead>
83+
<tbody id="pairTableBody" class="divide-y divide-slate-800"></tbody>
84+
</table>
11585
</div>
116-
</div>
86+
</section>
11787
</div>
118-
119-
<!-- Load the main module -->
120-
<script type="module">
121-
122-
import { tellBirthday } from './src/utils/utils.js';
123-
import { Person } from './src/components/person.js';
12488

125-
// Create a new Person instance with values defined in the HTML.
126-
const person = new Person('Alice', 40);
127-
document.getElementById('greetBtn').addEventListener('click', () => {
128-
alert(tellBirthday(person));
129-
});
89+
<div id="toastContainer" class="fixed bottom-6 left-1/2 -translate-x-1/2 md:left-auto md:right-8 md:translate-x-0 z-50 space-y-2 w-[calc(100%-2rem)] max-w-sm pointer-events-none"></div>
13090

91+
<script src="https://unpkg.com/[email protected]/dist/leaflet.js" integrity="sha384-cxOPjt7s7Iz04uaHJceBmS+qpjv2JkIHNVcuOrM+YHwZOmJGBXI00mdUXEq65HTH" crossorigin="anonymous"></script>
92+
<script type="importmap">
93+
{
94+
"imports": {
95+
"snap2map/index": "./src/index.js",
96+
"snap2map/calibrator": "./src/calibration/calibrator.js"
97+
}
98+
}
99+
</script>
100+
<script type="module">
101+
import 'snap2map/index';
131102
</script>
132-
133103
</body>
134-
</html>
104+
</html>

service-worker.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
const CACHE_NAME = 'snap2map-shell-v1';
2+
const SHELL_ASSETS = [
3+
'/',
4+
'/index.html',
5+
'/src/index.js',
6+
'/service-worker.js',
7+
];
8+
9+
self.addEventListener('install', (event) => {
10+
event.waitUntil(
11+
caches.open(CACHE_NAME).then((cache) => cache.addAll(SHELL_ASSETS)).then(() => self.skipWaiting()),
12+
);
13+
});
14+
15+
self.addEventListener('activate', (event) => {
16+
event.waitUntil(
17+
caches.keys().then((keys) => Promise.all(keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key)))).then(() => self.clients.claim()),
18+
);
19+
});
20+
21+
self.addEventListener('fetch', (event) => {
22+
const { request } = event;
23+
if (request.method !== 'GET') {
24+
return;
25+
}
26+
event.respondWith(
27+
caches.match(request).then((cached) => {
28+
if (cached) {
29+
return cached;
30+
}
31+
return fetch(request).then((response) => {
32+
const clone = response.clone();
33+
caches.open(CACHE_NAME).then((cache) => cache.put(request, clone));
34+
return response;
35+
});
36+
}).catch(() => caches.match('/index.html')),
37+
);
38+
});

0 commit comments

Comments
 (0)