Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 25 additions & 9 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ HTTP is recommended for map use because file:// cannot send a valid HTTP Referer

### File Structure

- js/whereshot-logic.js: pure calculations, no DOM or environment-dependent clock
- js/i18n.js: the Japanese and English dictionaries and the DOM layer; the only file that wording lives in
- js/whereshot-logic.js: pure calculations, no DOM, no environment-dependent clock, no wording
- js/main.js: DOM, file selection, UTC offset selection, SHA-256, report and preview
- js/exif-parser.js: File.arrayBuffer and ExifReader adapter
- js/sun-calculator.js: SunCalc adapter
Expand All @@ -57,6 +58,7 @@ window.WhereShotExifParser // Metadata extraction engine
window.WhereShotMapController // Map management
window.WhereShotSunCalculator // Solar position calculations
globalThis.WhereShotLogic // Pure calculations, also CommonJS-compatible
window.I18n // Japanese/English dictionaries and the DOM layer
window.WhereShotUtils // Shared utilities
window.WhereShotStations // Weather station data
```
Expand All @@ -68,13 +70,14 @@ All scripts are classic scripts with defer, in this order:
1. Leaflet 1.9.4 (cdnjs, existing SRI)
2. vendor/exifreader/exif-reader.min.js
3. vendor/suncalc/suncalc.js
4. data/stations.js
5. js/whereshot-logic.js
6. js/utils.js
7. js/exif-parser.js
8. js/sun-calculator.js
9. js/map-controller.js
10. js/main.js
4. js/i18n.js
5. data/stations.js
6. js/whereshot-logic.js
7. js/utils.js
8. js/exif-parser.js
9. js/sun-calculator.js
10. js/map-controller.js
11. js/main.js

### Initialization Flow

Expand Down Expand Up @@ -137,7 +140,7 @@ jmaHourlyUrl uses the UTC instant converted to Japan time, and hourly_s1.php.
sunReport receives SunCalc as an argument and uses a UTC instant.
sunPhaseKey uses altitude and azimuth, not sunrise/sunset times (polar regions can lack these).
Azimuth is clockwise from true north. Shadows below the horizon are null.
All direction labels use toCardinalJa.
Direction labels come from cardinalKey, translated at display time.

### 5. Map Management

Expand Down Expand Up @@ -173,9 +176,22 @@ img-src 'self' data: blob: https://tile.openstreetmap.org https://server.arcgiso
connect-src 'none'; font-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'
```

### 7. Japanese and English

Files: js/i18n.js plus the render* methods in main.js and map-controller.js.
The dictionaries hold every visible string; the pure logic returns {key, params}.
`?lang=`, then localStorage (`whereshot-language`), then `navigator.language` decide the language.
`setLanguage` fires `languagechange`; both render* entry points redraw from state, never from displayed text.
Never put `data-i18n` on a slot that JavaScript writes, or on an attribute that changes with state:
`apply()` overwrites unconditionally, so the result would roll back to the initial wording.
Station names and the Japanese filename patterns stay in Japanese; they are data, not wording.

### Must Not Do

- Do not use local-time Date getters, multi-argument Date, or locale formatting in whereshot-logic.js.
- Do not put Japanese or English wording in any file under js/ other than i18n.js.
- Do not keep state in a displayed string, and do not compare against one.
- Do not translate Exif tag names, segment names, map service names, coordinate notation, or station names.
- Do not use DOM, network, storage, randomness or current time inside pure logic.
- Do not change referrer to no-referrer; keep strict-origin-when-cross-origin for OSM.
- Do not restore the {s} subdomain in the OSM tile URL.
Expand Down
342 changes: 342 additions & 0 deletions README.en.md

Large diffs are not rendered by default.

18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ hub: true

# WhereShot - 撮影時刻・場所解析ツール

[English](README.en.md) · 日本語

![GitHub Repo stars](https://img.shields.io/github/stars/ipusiron/whereshot?style=social)
![GitHub forks](https://img.shields.io/github/forks/ipusiron/whereshot?style=social)
![GitHub last commit](https://img.shields.io/github/last-commit/ipusiron/whereshot)
Expand Down Expand Up @@ -76,6 +78,12 @@ Exifがない画像も、日時と位置を手動で補って検討できます
- UTCオフセットの根拠と日時ソースの整合度の表示
- ファイルのSHA-256と解析レポートのコピー

### 🌐 日本語・英語の切り替え

- ヘッダーのボタンで、画面・通知・解析レポートの全文を日本語と英語で切り替える
- `?lang=ja` / `?lang=en` での指定、保存した選択、ブラウザーの言語の順に決める
- 16方位・時間帯・日時ソースの名前も、解析結果を保ったまま訳し直す

### 🗺️ 地理空間分析
- **インタラクティブマップ**: Leafletによる地図表示
- **マルチレイヤー対応**: OpenStreetMap, 衛星画像, 地形図の切り替え
Expand Down Expand Up @@ -204,10 +212,11 @@ HEIC/HEIFなどをブラウザーが表示できない場合も、プレビュ
| Exif | ExifReader 4.12.0(自己ホスト) |
| 太陽位置 | SunCalc 1.9.0(自己ホスト) |
| 地図 | Leaflet 1.9.4(CDN+SRI)、3種類のタイル |
| 純粋ロジック | js/whereshot-logic.js(DOM・タイムゾーン非依存) |
| 純粋ロジック | js/whereshot-logic.js(DOM・タイムゾーン非依存、文言を持たない) |
| 文言 | js/i18n.js(日英の辞書とDOMへの適用) |
| 観測所 | stations.jsonと同じ54件をstations.jsで読み込む |

太陽の方位は真北を0°、時計回りにそろえ、16方位を日本語で表示します。
太陽の方位は真北を0°、時計回りにそろえ、16方位で表示します(日本語は「東南東」、英語は「ESE」)。
時間帯は高度と方位から決め、白夜・極夜にも対応します。
影の長さは物体の高さに対する比で、太陽が地平線の下なら影なしとします。
精度円はGPSHPositioningError(メートル)があるときだけ表示し、無次元のGPSDOPは使いません。
Expand All @@ -228,6 +237,7 @@ HEIC/HEIFなどをブラウザーが表示できない場合も、プレビュ

画像とExifの解析はブラウザー内で行い、画像・Exifを外部に送信しません。
入力した位置や解析結果、レポートをlocalStorageなどへ保存しません。
localStorageに保存するのは言語の選択(`whereshot-language`)だけです。
地図の表示には通信が必要で、撮影地点の周辺を見ていることがタイル配信元に伝わります。
機微な調査ではVPNなどの利用を検討してください。

Expand Down Expand Up @@ -275,6 +285,8 @@ npm test

GitHub Actionsがpushとpull_requestで自動実行します。
READMEの表と例、配色、HTML、配布ライブラリーのサイズとSHA-256も検証します。
日英の辞書のキー集合と差し込み名の一致、HTMLとスクリプトが使うキーの実在、
純粋ロジックに和文が残っていないことも検証します。
子プロセスをUTC・Asia/Tokyo・America/Los_Angeles・Pacific/Kiritimatiで起動し、日時推定・太陽位置・リンク・レポートが完全に一致することを確認します。

## 🧭 今後の候補
Expand Down Expand Up @@ -305,6 +317,7 @@ whereshot/
├── index.html # 画面とCSP
├── css/style.css # 不透明なダーク配色とレスポンシブ表示
├── js/
│ ├── i18n.js # 日英の辞書とDOMへの適用
│ ├── whereshot-logic.js # 環境非依存の計算と書式
│ ├── utils.js # File・UIの補助
│ ├── exif-parser.js # FileとExifReaderの接続
Expand All @@ -327,6 +340,7 @@ whereshot/
├── .nojekyll # Pagesで静的ファイルをそのまま配信
├── CLAUDE.md # 開発規則
├── LICENSE # MITライセンス
├── README.en.md # 英語版README
└── README.md # 本ファイル
```

Expand Down
58 changes: 44 additions & 14 deletions docs/api_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ class MapController {
```javascript
const WhereShotUtils = {
FileUtils: { formatFileSize, validateFile },
UIUtils: { showError, showSuccess, showLoading },
UIUtils: { showToast, showError, showSuccess, showLoading, retranslateToasts },
SecurityUtils: { clearSensitiveData, calculateHash }
}
```
Expand Down Expand Up @@ -255,38 +255,69 @@ interface SunData {
| isValidWall(w) | 有効な年月日時分秒かどうか |
| parseExifDateTime(str), inputValueToWall(v), utcMsToWall(ms, offsetMin) | 壁時計またはnull |
| parseOffset(str) | -720〜840のオフセット分またはnull |
| formatOffset(n), formatWall(w, offsetMin), formatUtc(ms), wallToInputValue(w) | 書式付き文字列 |
| formatOffset(n), formatWall(w), formatUtc(ms), wallToInputValue(w) | 書式付き文字列(不正入力は空文字) |
| wallToUtcMs(w, offsetMin), gpsDateTimeToUtcMs(date, time) | UTCミリ秒またはnull |
| inferOffsetFromGps(w, ms) | {offsetMin, residualSec}またはnull |
| decideOffset(input) | {offsetMin, source, residualSec, conflict} |
| longitudeOffsetHint(lng) | 表示用の目安(自動適用しない) |
| normalizeTags(tags) | ExifData |
| formatExposure(time), formatCamera(make, model) | 表示用文字列 |
| formatExposure(time), formatCamera(make, model) | 表示用文字列(不明はnull) |
| extractDatesFromFilename(name, nowMs) | {pattern, matched, hasTime, reliability, wallまたはutcMs}の配列 |
| estimateDateTime(input) | {sources, conflicts, notes, agreement, best, estimatedUtcMs, confidence} |
| sunReport(SunCalc, lat, lng, utcMs) | SunDataまたはnull |
| sunPhaseKey(alt, az), toCardinalJa(degrees) | 時間帯キー、日本語16方位 |
| sunPhaseKey(alt, az), phaseKey(phase), cardinalKey(degrees) | 時間帯と16方位のメッセージキー |
| isValidLatLng(lat, lng), decimalToDms(value, isLat) | 座標の検証、度分秒表記 |
| distanceM(lat1, lng1, lat2, lng2), bearing(lat1, lng1, lat2, lng2) | メートル、北基準の方位角 |
| destinationPoint(lat, lng, bearing, distance) | {lat, lng} |
| nearestStation(lat, lng, stations, maxKm=200) | {station, distanceKm}またはnull |
| nasaWorldviewUrl(lat, lng, wall), sunCalcOrgUrl(lat, lng, wall) | 現地の日付・時刻を使うURL |
| gsiMapUrl(lat, lng, photo), streetViewUrl(lat, lng, heading) | URLまたはnull |
| jmaHourlyUrl(lat, lng, utcMs, stations) | {url, station, distanceKm}またはnull |
| buildReport(d), formatInt(n) | レポート、3桁区切り文字列 |
| buildReport(d) | {key, params}の配列(15行または8行) |
| msg(key, params), formatInt(n) | メッセージ、3桁区切り文字列 |

公開定数はOFFSET_CHOICES(38件)、FILENAME_PATTERNS(11種)、SOURCE_LABEL、PHASE_JA、JMA_TOPです。
ロジックはDOM、現在時刻、crypto、ローカルタイムゾーンに依存しません。
公開定数はOFFSET_CHOICES(38件)、FILENAME_PATTERNS(11種)、SOURCE_KEYS、
OFFSET_SOURCE_KEYS、LOCATION_SOURCE_KEYS、CARDINAL_KEYS(16件)、PHASE_KEYS(7件)、JMA_TOPです。
ロジックはDOM、現在時刻、crypto、ローカルタイムゾーンに依存せず、**文言も持ちません**。
表示用の語はすべてjs/i18n.jsの辞書にあり、`I18n.t(key, params)`と`I18n.message({key, params})`で訳します。
ブラウザーの仮オフセットだけはDOM側で撮影日時の夏時間を含めて求めます。

### SHA-256とレポート

main.jsのcalculateFileHashはファイルのArrayBufferをWeb CryptoのSHA-256へ渡します。
buildReportにはfileName、fileSize、fileType、sha256、wall、offsetMin、offsetSource、residualSec、
dateSourceLabel、confidence、latitude、longitude、locationSource、directionDeg、sun、station、
stationKm、generatedAtUtcMsを渡します。
dateSource、confidence、latitude、longitude、locationSource、directionDeg、sun、station、
stationKm、generatedAtUtcMsを渡します。dateSourceは`{key, params}`で、
推定結果のsources[i].labelか`msg('report.manualDateTime')`を渡します。
戻り値は行ごとの`{key, params}`の配列で、`I18n.messages(lines)`で1本のテキストにします。
作成時刻はDOM側がDate.now()で用意します。結果はtextContentでpreに入れ、Clipboard APIが使えるときだけコピーします。

### 6. I18n (i18n.js)

日本語と英語の辞書、およびDOMへの適用層です。他のスクリプトは言語ごとの文字列を持ちません。

```javascript
const I18n = {
ja, en, // キーが完全に一致する2つの辞書
t(key, params), // 現在の言語で訳す(未知のキーはthrow)
tIn(language, key, params), // 言語を指定して訳す(テスト用)
message({ key, params }, language), // 入れ子の{key}も訳す
messages(list, language), // 改行で連結する
apply(root), init(), setLanguage(v), language
}
```

`apply()`は`data-i18n`のtextContentと、`data-i18n-aria-label` / `-title` / `-placeholder` /
`-alt` / `-content`の各属性を当てます。`<meta>`のcontentもここで訳します。
言語は`?lang=` → localStorage(`whereshot-language`) → `navigator.language`の順に決めます。
`setLanguage()`は`languagechange`イベントを発火し、main.jsの`renderLanguage()`と
map-controller.jsの`renderLanguage()`が状態から描き直します。

**状態を訳した文字列で持たないこと。**トーストはキーを`data-toast-key`に覚え、
プレビューの成否は`previewFailed`、SHA-256は`hashState`、地図の文言は`statusMessage`が持ちます。
JSが書き込むスロット(`#upload-title`・`#toggle-preview-btn`・`#weather-link`・`#direction-info`・
`#map-coordinates`ほか)には`data-i18n`を付けません。付けると言語切り替えで結果が初期文言に戻ります。

## ⚡ イベントシステム

WhereShotは、カスタムイベントを使用してモジュール間の通信を行います。
Expand Down Expand Up @@ -397,9 +428,8 @@ ExifReader 4.12.0が読み取れる形式に限り、読み取り失敗時もnor
1. **ファイル検証**:
```javascript
const validation = window.WhereShotUtils.FileUtils.validateFile(file);
if (!validation.isValid) {
throw new Error(validation.errors.join(', '));
}
// errorsはメッセージキーの配列。訳した文字列を連結して持ち回らない。
for (const key of validation.errors) window.WhereShotUtils.UIUtils.showError(key);
```

2. **座標の範囲チェック**:
Expand All @@ -417,13 +447,13 @@ ExifReader 4.12.0が読み取れる形式に限り、読み取り失敗時もnor
const result = await riskyOperation();
} catch (error) {
console.error('処理に失敗しました');
window.WhereShotUtils.UIUtils.showError('処理に失敗しました。内容を確認してください');
window.WhereShotUtils.UIUtils.showError('error.analyzeFailed');
}
```

2. **ユーザーフレンドリーなエラーメッセージ**:
```javascript
window.WhereShotUtils.UIUtils.showError('ファイルの読み込みに失敗しました。ファイル形式を確認してください。');
window.WhereShotUtils.UIUtils.showError('error.unsupportedType');
```

---
Expand Down
9 changes: 9 additions & 0 deletions docs/user_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ WhereShotは、画像から「いつ・どこで撮られたか?」を解析
- **多角的解析**: Exif情報、太陽位置、地図表示を組み合わせた検証
- **直感的なUI**: ドラッグ&ドロップで簡単操作
- **外部データ連携**: 衛星画像、気象データとの照合が可能
- **日本語・英語の切り替え**: ヘッダーのボタンで画面と解析レポートの全文を切り替え

### 表示言語

ヘッダーの言語ボタンを押すと、画面・通知・解析レポートが日本語と英語で切り替わります。
入力した日時や位置、解析結果は切り替えても消えません。
URLに`?lang=en`(または`?lang=ja`)を付けても指定できます。
指定がない場合は、前回選んだ言語、次にブラウザーの言語を使います。
選んだ言語はlocalStorageに`whereshot-language`として保存され、画像やExifは保存されません。

---

Expand Down
Loading
Loading