diff --git a/CLAUDE.md b/CLAUDE.md index d72bf78..2b522a0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 @@ -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 ``` @@ -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 @@ -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 @@ -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. diff --git a/README.en.md b/README.en.md new file mode 100644 index 0000000..1e5524e --- /dev/null +++ b/README.en.md @@ -0,0 +1,342 @@ +# WhereShot - Capture Time & Place Analyzer + +English · [日本語](README.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) +![GitHub license](https://img.shields.io/github/license/ipusiron/whereshot) +[![GitHub Pages](https://img.shields.io/badge/demo-GitHub%20Pages-blue?logo=github)](https://ipusiron.github.io/whereshot/) + +**Day013 - Security Tools 100 with Generative AI** + +WhereShot is an OSINT helper for estimating and verifying when and where a photo was taken. +It combines Exif extraction, sun position calculation, bearing on a map, and links to weather records and satellite imagery. +Photos without Exif can still be examined by filling in the time and the location by hand. + +## 🌐 Demo + +👉 [Open WhereShot](https://ipusiron.github.io/whereshot/) + +## 📸 Screenshots + +![Exif and the time estimate](assets/screenshot.png) +> *Exif, SHA-256 and a 95% consistency estimate for the Marugame Castle sample* + +![Local time and sun position](assets/screenshot3.png) +> *The browser runs in America/Los_Angeles. UTC+09:00 is inferred from GPS, and the sun altitude reads 64.0°* + +![Mobile layout](assets/screenshot4.png) +> *The sample loaded at a viewport width of 390px* + +## ✨ Features + +### 📊 Metadata analysis + +- ExifReader extracts the capture time, GPS coordinates, camera settings and lens information +- The basis of the UTC offset and the consistency of the date sources are shown +- The file SHA-256 and the analysis report can be copied + +### 🌐 Japanese and English + +- One header button switches the whole interface, the notifications and the analysis report +- The language is taken from `?lang=ja` / `?lang=en`, then the saved choice, then the browser +- Compass points, times of day and source names are re-translated without losing the results + +### 🗺️ Geospatial view + +- **Interactive map**: rendered with Leaflet +- **Multiple layers**: OpenStreetMap, satellite imagery and terrain + +### 🌞 Solar geometry + +- **Sun position**: computed with the bundled SunCalc +- **Shadow analysis**: the shadow length and direction follow from the altitude and azimuth +- **Time checking**: compare the shadows in the photo with the computed ones + +### 🧭 Bearing + +- **Direction arrows**: the bearing recorded in Exif, and a direction picked by hand on the map + +### 🌐 External data + +Links are generated for you so that the follow-up work is quick. + +- **[NASA Worldview](https://worldview.earthdata.nasa.gov/)**: satellite imagery and weather layers +- **[GSI Maps](https://maps.gsi.go.jp/)**: aerial photos and topographic maps of Japan +- [JMA past weather](https://www.data.jma.go.jp/stats/etrn/index.php): hourly records of the nearest station + +The nearest of 54 stations is chosen; when none lies within 200km the link points at the JMA top page instead. +Outside Japan, and for Ogasawara, Amami, Hachijojima and Daito, no station is available. + +## 📖 How to use + +1. Open the [demo page](https://ipusiron.github.io/whereshot/), or open index.html directly. +2. Drag and drop an image, then read the Exif tags and the time estimate. +3. Check the UTC offset and the analysis time, and change them if needed. +4. Look at the capture point on the map. Without GPS, the manual location mode turns on, so click the map. +5. Check the sun position and the shadows, and study the surroundings through the external links. +6. Copy the SHA-256 and the analysis result if you need them. + +"Set the location by hand" and "Set the camera direction" are never active at the same time. +With both off, clicking the map does not move the marker. +The preview can be shown and hidden with its button. + +### Worked example + +These are the results for the [Marugame Castle sample](assets/2016-07-24%2010.33.57.jpg). + +| Item | Value | +|---|---| +| Capture time (local) | 2016/07/24 10:33:57 | +| Capture time (UTC) | 2016-07-24 01:33:57 UTC | +| UTC offset | +09:00 (gap with the GPS time, residual -1 s) | +| Sun altitude | 64.0° | +| Sun azimuth | 117.3° (ESE) | +| Shadow direction | 297.3° (WNW) | +| Shadow length | 0.49× the height | +| Consistency | 95% | +| Nearest station | 高松 / Takamatsu (about 23 km) | +| SHA-256 | a08e3c4742a0a910e2df04a71f0b165fd87281695a26ac7fe5056a15752aaff8 | + +## 🕒 Time and time zones + +An Exif capture time is the local time at the site, and DateTimeOriginal carries no time zone of its own. +The "wall clock" of year, month, day, hour, minute and second is combined with a UTC offset to reach a UTC instant. +A camera clock is not necessarily set to the local time; a traveller's camera needs checking. + +Candidates for the UTC offset are taken in this order, and the basis is shown on screen. + +1. The Exif OffsetTimeOriginal +2. The gap between the local time and the GPS time in UTC, rounded to 15 minutes (rejected when the residual exceeds 5 minutes) +3. The browser time zone for that date, as a provisional value + +For the sample, local 10:33:57 against GPS 01:33:58 UTC yields UTC+09:00 with a residual of -1 second. +A disagreement between the Exif offset and the GPS estimate raises a warning. +The offset can be changed by hand, and the analysis time keeps its seconds. + +The hint derived from the longitude is only displayed, never applied on its own; it may differ from standard time or daylight saving. + +## 🗂️ Supported formats and file names + +### Supported formats + +| Format | MIME | +|---|---| +| JPEG | image/jpeg | +| PNG | image/png | +| TIFF | image/tiff | +| WebP | image/webp | +| HEIC/HEIF | image/heic, image/heif | + +The limit is 100MB. Video, such as MP4, is out of scope. +When the browser cannot render HEIC/HEIF, the preview is skipped and the metadata analysis continues. +For damaged content, or a file with no Exif, a notice says that the metadata could not be read. + +### File name formats + +| Example | Pattern | Date read | Reference | +|---|---|---|---| +| IMG_20240101_123456.jpg | ymd-hms-compact | 2024/01/01 12:34:56 | local | +| PXL_20240101_123456789.jpg | pxl-utc | 2024-01-01 12:34:56 UTC | UTC | +| Screenshot_2024-01-01-12-34-56.png | ymd-hms-sep | 2024/01/01 12:34:56 | local | +| signal-2024-01-01-123456.jpg | ymd-sep-hms-compact | 2024/01/01 12:34:56 | local | +| WhatsApp Image 2024-01-01 at 12.34.56.jpeg | ymd-hms-sep | 2024/01/01 12:34:56 | local | +| 20160724103357.jpg | ymdhms-14 | 2016/07/24 10:33:57 | local | +| 2024年1月1日12時34分56秒.jpg | ja-full | 2024/01/01 12:34:56 | local | +| IMG-20240101-WA0001.jpg | ymd-8 | 2024/01/01 12:00:00 | local, date only | +| 1609459200.jpg | unix-s | 2021-01-01 00:00:00 UTC | UTC | + +Eleven patterns are tried from the strongest down, and an accepted span of characters is never read twice. +The 12:00:00 used for a date-only name is a placeholder, not a capture time. +An invalid calendar date is rejected rather than rolled into the next month. + +## 🎯 Use cases + +### OSINT and verification + +- **Checking social posts**: whether the claimed time and place hold together +- **Spotting recycled photos**: finding the earlier use of an image +- **Reviewing evidence**: judging how much weight an image can carry + +### Incidents and disasters + +- **Reading the scene**: clues to the place and the time in photos from a disaster area +- **Newsroom checks**: whether the photo matches the reported situation +- **Building a timeline**: working through images one at a time + +### Teaching and training + +- **Learning OSINT**: hands-on practice with open sources +- **Media literacy**: judging whether a digital image is what it claims to be +- **Security awareness**: understanding the risk carried by metadata + +## 🔬 How it is built + +| Part | Implementation | +|---|---| +| Interface | vanilla JavaScript, HTML, CSS | +| Exif | ExifReader 4.12.0 (self-hosted) | +| Sun position | SunCalc 1.9.0 (self-hosted) | +| Map | Leaflet 1.9.4 (CDN with SRI), three tile sets | +| Pure logic | js/whereshot-logic.js (no DOM, no time zone, no wording) | +| Wording | js/i18n.js (the Japanese and English dictionaries, and the DOM layer) | +| Stations | the same 54 entries as stations.json, loaded through stations.js | + +Azimuth is measured clockwise from true north and shown as one of 16 compass points ("ESE" in English, 「東南東」 in Japanese). +The time of day follows from the altitude and the azimuth, so polar day and polar night are covered. +Shadow length is a ratio against the height of the object, and there is no shadow while the sun sits below the horizon. +The accuracy circle appears only when GPSHPositioningError, in meters, is present; the dimensionless GPSDOP is never used. +Shutter speed comes from ExposureTime (0.000501 s reads as 1/1996s). + +### Consistency + +The weights are 0.95 for the Exif capture time, 0.9 for the GPS time, 0.85 for the Exif digitized time, +0.4 to 0.6 for a file name, 0.5 for the Exif modification time and 0.3 for the file modification time. +Sources that carry a time win, and the heaviest of those is adopted. +Among the sources other than the two modification times, a gap over one hour (24 hours when a date-only source is involved) counts as a conflict. + +Consistency = round(weight of the adopted source × (0.5 + 0.5 × share of agreeing pairs) × 100) ÷ 100. +With no pair to compare the share is 1, and with no source at all the consistency is 0. The ceiling is 95%. +A drifting modification time becomes a note about saving, editing or copying. +The value measures how little the metadata disagrees with itself; it is not a guarantee of authenticity. + +## 🔒 Security and privacy + +The image and its Exif are analyzed inside the browser, and neither is ever uploaded. +Positions you enter, the analysis results and the report are not written to localStorage or anywhere similar. +The only thing stored in localStorage is the language you picked (`whereshot-language`). +Showing the map does need network access, and the tile provider learns that you are looking around the capture point. +For a sensitive investigation, consider a VPN. + +### Where data goes + +| When | Where | What is revealed | +|---|---|---| +| On startup | cdnjs.cloudflare.com | two Leaflet files are fetched (your IP address and the origin) | +| Every time the map is drawn or moved | tile.openstreetmap.org | the tile numbers of the visible area, and the origin | +| When satellite imagery is selected | server.arcgisonline.com | the tile numbers of the visible area, and the origin | +| When the terrain map is selected | *.tile.opentopomap.org | the tile numbers of the visible area, and the origin | +| Only when you follow an external link | NASA, GSI, JMA, Google, suncalc.org | the coordinates and the date inside the URL (the Google Images entry point carries neither) | + +No map is created on startup, so no tile is requested until an image is loaded. +The map loads only the one selected layer. +The OSM tile URL is `https://tile.openstreetmap.org/{z}/{x}/{y}.png`. + +The countermeasures are a CSP without 'unsafe-inline', SRI for Leaflet, self-hosted ExifReader and SunCalc with hash checks, and rendering through textContent. +File names and Exif values are never interpreted as HTML, and coordinates and times are never written to the console. +External links carry rel="noopener noreferrer". +The referrer policy is strict-origin-when-cross-origin: the OSM tile terms require a valid Referer, so no-referrer is not used, and only the origin is sent over HTTP. +Because file:// cannot send a valid HTTP Referer, serving over HTTP is recommended when you use the map. + +### Tile attribution and licensing + +- OpenStreetMap: © OpenStreetMap contributors. The map data is ODbL, under the standard tile usage policy. +- OpenTopoMap: © OpenStreetMap contributors, SRTM, OpenTopoMap. The map itself is offered under CC-BY-SA. +- Esri World Imagery: offered under the Esri Master License Agreement. Attribution is Esri, Vantor, Earthstar Geographics, and the GIS User Community. + +## ⚠️ Notes + +This tool exists to support OSINT work. It is not meant for invading privacy or for illegitimate surveillance. +Please follow the laws and the ethical guidelines that apply to you. + +Exif, file names and modification times can all be rewritten. Do not treat a result from this tool alone as proof of a capture. +Images posted on social networks usually have their Exif stripped, so a position or a time is often unavailable. +A bearing recorded against magnetic north is not corrected. Keep that difference in mind when comparing it with a sun azimuth or with Street View. + +## 🧪 Tests + +Node 22 or later. There are no npm dependencies, and no network access is needed while the tests run. + +```bash +npm test +``` + +GitHub Actions runs them on push and pull_request. +They also check the tables and worked examples in both READMEs, the palette, the HTML, and the size and SHA-256 of the bundled libraries. +Child processes start in UTC, Asia/Tokyo, America/Los_Angeles and Pacific/Kiritimati to confirm that the time estimate, the sun position, the links and the report come out identical. +For the two languages they check that the key sets and the placeholder names match, that every key used by the HTML and the scripts exists, and that no Japanese is left in the pure logic. + +## 🧭 Possible next steps + +The following are not implemented. + +- Coordinate conversions +- Season checks, field-of-view calculation, landmark matching +- Magnetic declination correction +- Bulk processing, tabular export, side-by-side comparison + +## 🔗 References + +- [ExifReader](https://github.com/mattiasw/ExifReader) +- [SunCalc](https://github.com/mourner/suncalc) +- [SunCalc.org](https://www.suncalc.org/) +- [Leaflet](https://leafletjs.com/) +- [Google Maps URLs](https://developers.google.com/maps/documentation/urls/guide) +- [OSM tile usage policy](https://operations.osmfoundation.org/policies/tiles/) +- [OpenStreetMap copyright and licence](https://www.openstreetmap.org/copyright) +- [About OpenTopoMap](https://opentopomap.org/about) +- [Esri World Imagery](https://www.arcgis.com/home/item.html?id=10df2279f9684e4a9f6a7f08febac2a9) + +## 📁 Directory layout + +```text +whereshot/ +├── index.html # the interface and the CSP +├── css/style.css # opaque dark palette and responsive layout +├── js/ +│ ├── i18n.js # the Japanese and English dictionaries, and the DOM layer +│ ├── whereshot-logic.js # environment-independent calculation and formatting +│ ├── utils.js # File and UI helpers +│ ├── exif-parser.js # the bridge between File and ExifReader +│ ├── sun-calculator.js # the bridge to SunCalc +│ ├── map-controller.js # Leaflet and the map interactions +│ └── main.js # the interface controller +├── assets/ # the sample image and the screenshots +├── data/ +│ ├── stations.json # the authoritative 54 stations +│ └── stations.js # the same data, for file:// +├── vendor/ +│ ├── exifreader/ # ExifReader itself, LICENSE and README +│ └── suncalc/ # SunCalc itself, LICENSE and README +├── docs/ +│ ├── user_guide.md # the user guide +│ └── api_reference.md # the internal API +├── test/ # the node:test suite and its fixtures +├── .github/workflows/test.yml # the automated tests +├── package.json # the npm test definition +├── .nojekyll # serve static files as they are on Pages +├── CLAUDE.md # the development rules +├── LICENSE # the MIT licence +├── README.en.md # this file +└── README.md # the Japanese README +``` + +## 💻 Requirements + +Current Chrome, Edge, Firefox and Safari are assumed. File.arrayBuffer, Web Crypto and dialog are used. +Whether HEIC previews depends on the browser. +Opening index.html over file:// works for the analysis, with the same 54 stations as over HTTP. +To serve it over HTTP, run the following in the repository root and open http://localhost:8000. + +```bash +python -m http.server 8000 +``` + +Node 22 or later is needed for the tests. There is no build step. + +## 📄 Licence + +The tool itself is MIT licensed; see [LICENSE](LICENSE). +The bundled ExifReader 4.12.0 is MPL-2.0 and SunCalc 1.9.0 is BSD-2-Clause. +Each ships under vendor/ with its LICENSE and a README recording its source and hash. +Map data and tiles are covered by the licence and the terms of their provider. + +## 🛠 About this tool + +This tool was built as part of the "Security Tools 100 with Generative AI" project, +in which security-related tools are created and published over 100 days with the help of AI. + +For the project and the other tools, see the page below. + +🔗 [https://akademeia.info/?page_id=42163](https://akademeia.info/?page_id=42163) diff --git a/README.md b/README.md index dffa411..b496eee 100644 --- a/README.md +++ b/README.md @@ -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) @@ -76,6 +78,12 @@ Exifがない画像も、日時と位置を手動で補って検討できます - UTCオフセットの根拠と日時ソースの整合度の表示 - ファイルのSHA-256と解析レポートのコピー +### 🌐 日本語・英語の切り替え + +- ヘッダーのボタンで、画面・通知・解析レポートの全文を日本語と英語で切り替える +- `?lang=ja` / `?lang=en` での指定、保存した選択、ブラウザーの言語の順に決める +- 16方位・時間帯・日時ソースの名前も、解析結果を保ったまま訳し直す + ### 🗺️ 地理空間分析 - **インタラクティブマップ**: Leafletによる地図表示 - **マルチレイヤー対応**: OpenStreetMap, 衛星画像, 地形図の切り替え @@ -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は使いません。 @@ -228,6 +237,7 @@ HEIC/HEIFなどをブラウザーが表示できない場合も、プレビュ 画像とExifの解析はブラウザー内で行い、画像・Exifを外部に送信しません。 入力した位置や解析結果、レポートをlocalStorageなどへ保存しません。 +localStorageに保存するのは言語の選択(`whereshot-language`)だけです。 地図の表示には通信が必要で、撮影地点の周辺を見ていることがタイル配信元に伝わります。 機微な調査ではVPNなどの利用を検討してください。 @@ -275,6 +285,8 @@ npm test GitHub Actionsがpushとpull_requestで自動実行します。 READMEの表と例、配色、HTML、配布ライブラリーのサイズとSHA-256も検証します。 +日英の辞書のキー集合と差し込み名の一致、HTMLとスクリプトが使うキーの実在、 +純粋ロジックに和文が残っていないことも検証します。 子プロセスをUTC・Asia/Tokyo・America/Los_Angeles・Pacific/Kiritimatiで起動し、日時推定・太陽位置・リンク・レポートが完全に一致することを確認します。 ## 🧭 今後の候補 @@ -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の接続 @@ -327,6 +340,7 @@ whereshot/ ├── .nojekyll # Pagesで静的ファイルをそのまま配信 ├── CLAUDE.md # 開発規則 ├── LICENSE # MITライセンス +├── README.en.md # 英語版README └── README.md # 本ファイル ``` diff --git a/docs/api_reference.md b/docs/api_reference.md index 924b098..639edf1 100644 --- a/docs/api_reference.md +++ b/docs/api_reference.md @@ -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 } } ``` @@ -255,17 +255,17 @@ 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} | @@ -273,20 +273,51 @@ interface SunData { | 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`の各属性を当てます。``の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は、カスタムイベントを使用してモジュール間の通信を行います。 @@ -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. **座標の範囲チェック**: @@ -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'); ``` --- diff --git a/docs/user_guide.md b/docs/user_guide.md index 1e2495f..596bb79 100644 --- a/docs/user_guide.md +++ b/docs/user_guide.md @@ -27,6 +27,15 @@ WhereShotは、画像から「いつ・どこで撮られたか?」を解析 - **多角的解析**: Exif情報、太陽位置、地図表示を組み合わせた検証 - **直感的なUI**: ドラッグ&ドロップで簡単操作 - **外部データ連携**: 衛星画像、気象データとの照合が可能 +- **日本語・英語の切り替え**: ヘッダーのボタンで画面と解析レポートの全文を切り替え + +### 表示言語 + +ヘッダーの言語ボタンを押すと、画面・通知・解析レポートが日本語と英語で切り替わります。 +入力した日時や位置、解析結果は切り替えても消えません。 +URLに`?lang=en`(または`?lang=ja`)を付けても指定できます。 +指定がない場合は、前回選んだ言語、次にブラウザーの言語を使います。 +選んだ言語はlocalStorageに`whereshot-language`として保存され、画像やExifは保存されません。 --- diff --git a/index.html b/index.html index a747260..a445afc 100644 --- a/index.html +++ b/index.html @@ -4,9 +4,25 @@ WhereShot - 撮影時刻・場所解析ツール - + + - + + + + + + + + + + + + - + - + - + - +

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

- - + + +
@@ -51,42 +69,46 @@

📸
-

画像をドラッグ&ドロップ

+

-

または

- 対応形式: JPEG, PNG, TIFF, WebP, HEIC/HEIF(100MB以下) +

または +

+ 対応形式: JPEG, PNG, TIFF, WebP, HEIC/HEIF(100MB以下)
- +