Skip to content

Latest commit

 

History

History
312 lines (236 loc) · 15.1 KB

File metadata and controls

312 lines (236 loc) · 15.1 KB

GPS Map Camera — Web App Build Prompt

Build a GPS Map Camera web application (pure HTML + CSS + JavaScript, no frameworks) that lets users upload a photo, enter GPS coordinates and location details, and generates a geotagged image with a professional location overlay — similar to the "GPS Map Camera" Android app.


Requirements

  1. When a user uploads a photo and enters latitude and longitude, it should auto-fill the city, state, country, pincode, and address fields using reverse geocoding (Nominatim) from the coordinates.
  2. The user should also be able to enter the city, state, country, pincode, and address manually using separate input fields for each.
  3. When a user enters a pincode, it should auto-fill the city, state, and country using the Zippopotam.us API (tries 16 country codes sequentially).
  4. A native datetime-local picker should be provided for date and time, pre-filled with the current date and time on page load.
  5. When the user clicks "Generate GeoTagged Image", the app should overlay a two-box info bar (satellite map + location details) at the bottom of the photo and allow downloading the result with embedded GPS EXIF metadata.
  6. The entire app should be a single-page web app using plain HTML, CSS, and JavaScript — no frameworks.

App Overview

A single-page web app with three sections:

  1. Input Section — Upload photo, enter lat/lng (with lookup button), city/state/country, pincode, address, and date/time
  2. Loading Section — Spinner while generating
  3. Result Section — Preview the composited image on a canvas + download button + new photo button

The downloaded JPEG must have GPS EXIF metadata embedded so platforms like Google Photos, Instagram, etc. detect the location.


Tech Stack

  • Pure HTML/CSS/JS — no React, no build tools, no server-side code
  • piexif.js (CDN: https://cdn.jsdelivr.net/npm/piexifjs@1.0.6/piexif.min.js) for EXIF GPS embedding
  • Google Satellite Hybrid tiles (https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}) with OSM fallback
  • flagcdn.com for country flag images (https://flagcdn.com/w80/{countryCode}.png)
  • Nominatim (OpenStreetMap) for reverse geocoding from coordinates
  • Zippopotam.us for pincode-to-location lookup
  • Canvas API for compositing the overlay onto the photo
  • Serve with any static file server (python -m http.server 8765 works fine)

File Structure

index.html  — Page markup + form
styles.css  — Dark-theme styles
app.js      — All application logic (~700 lines)

UI Design (Dark Theme)

Color Scheme

  • Background: #0f1117
  • Card/input backgrounds: #1a1d27
  • Accent green: #4CAF50 (buttons, logo gradient)
  • Text: #e0e0e0 primary, #888 secondary/labels
  • Borders: #333 inputs, #444 buttons/upload area
  • Border radius: 14px cards/upload, 10px inputs/buttons

Layout

  • Max-width 800px, centered, with 24px 16px padding
  • Header: SVG globe icon + "GPS Map Camera" title (gradient green text #4CAF50 → #81C784) + subtitle: "Upload a photo and enter coordinates to generate a geotagged image"
  • Upload area: Dashed border box (2px dashed #444), drag-and-drop + click to browse, with an SVG image icon
  • Preview thumbnail: When file is selected, collapse the upload box and show a 56×56px thumbnail, filename, and × clear button
  • Result section: Canvas preview with rounded corners, "Download Image" button (green, with download SVG icon) + "New Photo" button

Form Fields (order matters — top to bottom)

  1. Row 1 — Coordinates + Lookup Button (single row, 3 columns):

    • Latitude (type="number", step="any", placeholder "Enter latitude")
    • Longitude (type="number", step="any", placeholder "Enter longitude")
    • Lookup button (magnifying glass 🔍 icon, flex:0, aligned to the bottom of the row via align-self:flex-end). Disabled until both lat/lng are valid numbers. Clicking it triggers reverse geocoding to auto-fill all fields below.
  2. Auto-fill notice — A small green text line "✓ Location details auto-filled from coordinates" that appears for 4 seconds after a successful coordinate lookup, then hides. Class .auto-fill-notice, color #4CAF50, font-size 0.78rem.

  3. Row 2 — City / State / Country (3-column row, class .form-row-3):

    • City (type="text", placeholder "Enter city name")
    • State (type="text", placeholder "Enter state")
    • Country (type="text", placeholder "Enter country")
  4. Row 3 — Pincode + Address (2-column row):

    • Pincode (type="text", flex:0.4, maxlength="10", placeholder "Enter pincode") — wrapped in a .pincode-wrapper div with a status indicator <span> that shows:
      • spinning (class .loading) while looking up
      • green (class .success) on match
      • red (class .error) on failure
    • Address (type="text", flex:1, placeholder "Enter full street address")
  5. Date / Time — Native <input type="datetime-local"> with color-scheme: dark for dark theme compatibility. Pre-filled with current local date/time on page load.

  6. Generate Button — Full-width green primary button (#4CAF50), disabled until a file is uploaded AND both lat/lng have valid numbers.

All placeholders use generic descriptive text — no example data or personal information.

Responsive

  • .form-row and .form-row-3 stack vertically below 500px
  • Logo title shrinks to 1.3rem

Auto-Fill from Coordinates (Nominatim Reverse Geocoding)

Trigger Methods

  • Manual: User clicks the magnifying glass lookup button (requires valid lat/lng).
  • Automatic: When lat/lng change events fire and the city field is still empty, a debounced lookup (800ms) triggers automatically. It does NOT overwrite user-entered data.

Reverse Geocode API

GET https://nominatim.openstreetmap.org/reverse?format=json&lat={lat}&lon={lng}&zoom=18&addressdetails=1&accept-language=en
Headers: User-Agent: GPSMapCamera/1.0

Fields Populated

From the response data.address:

  • City: city || town || village || county
  • State: state || region
  • Country: country
  • Pincode: postcode
  • Address (only if currently empty): Composed from road/pedestrian, suburb/neighbourhood, city, state, postcode, country joined with commas

UI Feedback

  • Lookup button icon changes to spinning during request, reverts to 🔍 when done.
  • Green auto-fill notice shown for 4 seconds on success.

Pincode Auto-Lookup (Zippopotam.us)

When the user types in the pincode field:

  1. Debounce 500ms after each keystroke. Ignore if fewer than 4 characters.
  2. Show spinning status indicator.
  3. Try the pincode against 16 countries in order: in, us, gb, ca, au, de, fr, it, es, br, mx, jp, pk, bd, lk, np.
    GET https://api.zippopotam.us/{countryCode}/{pincode}
    
  4. On first successful match: populate City (place name), State (state), Country (country). Show (green).
  5. If no country matches: Show (red).

Country Code Lookup (getCountryCode)

Maps country names (lower-cased) to ISO 3166-1 alpha-2 codes. Supports 40+ countries including: India, USA/United States, UK/United Kingdom/Great Britain, Australia, Canada, Germany, France, China, Japan, Brazil, Russia, South Korea, Italy, Spain, Mexico, Indonesia, Turkey, Saudi Arabia, South Africa, Nigeria, Egypt, Pakistan, Bangladesh, Sri Lanka, Nepal, Thailand, Vietnam, Malaysia, Singapore, Philippines, UAE/United Arab Emirates, Netherlands, Sweden, Norway, Denmark, Finland, Switzerland, Portugal, Poland, Ireland, New Zealand, Argentina, Colombia, Kenya, Israel, Qatar.

The country code is used to fetch the flag image from flagcdn.com.


Photo Overlay Layout (Two-Box Design)

The overlay is rendered at the bottom of the photo using Canvas API. It consists of two separate rounded boxes side by side. All sizes scale proportionally:

S = max(imageWidth, imageHeight) / 1200

Visual Layout

┌──────────────────────────────────────────────────────────────────┐
│                         (original photo)                        │
│                                                                  │
│  ┌───────────────┐  ┌────────────────────────────────────────┐  │
│  │               │  │  City, State, Country  🇮🇳              │  │
│  │   Satellite   │  │  Full address line wrapping…           │  │
│  │     Map       │  │  Lat 17.3840000°  Long 78.4560000°     │  │
│  │   (zoom 17)   │  │  Friday, 27/02/2026 11.32 AM GMT +5.30│  │
│  └───────────────┘  └────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────────┘

Box 1 — Satellite Map (Left)

  • Shows satellite map tiles centered on the coordinates with a red Google Maps-style pin marker at center
  • Pin has: drop shadow ellipse, red body (#e53935) with dark stroke (#b71c1c), white dot highlight
  • "Google" label at bottom-left of map (white text with shadow)
  • Map size: min(240 * S, imageHeight * 0.28, imageWidth * 0.30)
  • Rounded corners with white semi-transparent border (rgba(255,255,255,0.3))

Box 2 — Location Info (Right)

  • Semi-transparent dark background: rgba(0, 0, 0, 0.65)
  • Subtle white border: rgba(255,255,255,0.15)
  • Width fills remaining space: totalContentW − mapBoxSize − gap
  • Height is computed from content so it grows if the address wraps to multiple lines
  • Content (top to bottom):
    1. City, State, Country + flag image — bold white, font size 38 * S
    2. Full address — light gray (#d0d0d0), font size 26 * S, word-wrapping supported
    3. Lat/Long — monospace font (Consolas/Courier New), #b8c4ce, font size 26 * S, format: Lat 17.3840000° Long 78.4560000° (7 decimal places with degree symbol)
    4. Date/Time#b8c4ce, font size 24 * S, format: Friday, 27/02/2026 11.32 AM GMT +5.30

"GPS Map Camera" Badge

  • Floats above the info box (top-right corner)
  • Dark semi-transparent rounded rectangle (rgba(0,0,0,0.55), radius 6*S)
  • Small green globe icon (circle + ellipse + horizontal line, stroked in #4CAF50)
  • "GPS Map Camera" text in white bold, font size 16 * S

Spacing & Scaling

  • Gap between boxes: 12 * S
  • Margin from image edges: 20 * S
  • Inner padding in info box: 22 * S
  • Corner radius: 12 * S
  • Both boxes are the same height = max(mapBoxSize, infoBoxHeight)
  • Both boxes are horizontally centered on the image

Country Flag

Fetched from https://flagcdn.com/w80/{code}.png and drawn inline after the city text, sized to headerSize * 0.85 height with 1.5× width ratio.


Map Tile Loading

  1. Convert lat/lng to tile coordinates at zoom 17 using Mercator projection
  2. Load a 3×3 grid of 256px tiles around the center tile
  3. Use Google satellite hybrid: https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}
  4. Fallback to OSM: https://tile.openstreetmap.org/{z}/{x}/{y}.png if Google fails
  5. Composite tiles on a 300×300 off-screen canvas, offset to center the exact coordinate
  6. Draw the red pin marker on top
  7. All tile images loaded with crossOrigin = 'anonymous'

Date/Time Handling

Input

Native <input type="datetime-local"> with color-scheme: dark.

Pre-fill on Page Load

An IIFE runs on load that sets the picker to the current local date/time:

const now = new Date();
const offset = now.getTimezoneOffset();
const local = new Date(now.getTime() - offset * 60000);
dateInput.value = local.toISOString().slice(0, 16);

Format for Overlay (formatDate)

Reads the picker value as a Date object. If empty, uses new Date().

Output pattern:

DayName, DD/MM/YYYY HH.MM AM/PM GMT ±H.MM

Example: Friday, 27/02/2026 11.32 AM GMT +5.30


GPS EXIF Embedding (Critical Feature)

When the user clicks "Download", embed GPS coordinates into the JPEG EXIF data using piexif.js so the image is location-aware.

Conversion: Decimal Degrees → DMS Rationals

EXIF GPS uses DMS (Degrees/Minutes/Seconds) as arrays of rational pairs [numerator, denominator].
Each rational is a uint32 pair (max 4,294,967,295).

Algorithm for maximum precision:
1. d = floor(abs(degrees))
2. minFloat = (abs - d) * 60
3. m = floor(minFloat)
4. sFloat = (minFloat - m) * 60
5. Maximize seconds denominator:
   - If sFloat >= 1: sDenom = floor(4294967295 / ceil(sFloat))
   - If sFloat > 0 but < 1: sDenom = 4294967295
   - If sFloat == 0: sDenom = 1
6. sNum = round(sFloat * sDenom)
7. Safety check: if sNum > 4294967295, decrement sDenom and recalculate

Result: [[d, 1], [m, 1], [sNum, sDenom]]

EXIF Structure

GPS IFD:
  GPSLatitudeRef: 'N' or 'S'
  GPSLatitude: [[d,1], [m,1], [sNum, sDenom]]
  GPSLongitudeRef: 'E' or 'W'
  GPSLongitude: [[d,1], [m,1], [sNum, sDenom]]
  GPSAltitudeRef: 0
  GPSAltitude: [0, 1]

0th IFD:
  Software: 'GPS Map Camera Web'

This gives sub-millimetre precision while staying inside the uint32 rational bounds required by EXIF.

If EXIF embedding fails, fall back to downloading without EXIF (don't block the user). Download filename: geotagged_photo.jpg, quality 0.95.


Key Behaviors

  1. Upload area hides when file is selected, shows thumbnail instead
  2. Generate button stays disabled until a file is uploaded AND both lat/lng have valid numbers
  3. Lookup button stays disabled until both lat/lng have valid numbers
  4. Auto-fill from coordinates only triggers if city field is empty (won't overwrite user edits)
  5. Pincode lookup tries 16 countries sequentially and stops on first match
  6. "New Photo" returns to input section (preserves form data)
  7. Canvas output matches the original photo dimensions exactly
  8. All overlay elements scale proportionally to the image size via S
  9. Flag images loaded from CDN in parallel with map tiles (Promise.all)
  10. Address word-wrapping dynamically adjusts the info box height
  11. Both boxes share the same height (max of map vs info content)
  12. The form reads separate City, State, Country fields directly — no address string parsing needed
  13. If no address text is provided, fullAddress falls back to city, state, country joined with commas

Summary

This app takes a photo, overlays a professional GPS location stamp (satellite map + location details in two rounded boxes), and embeds real GPS EXIF metadata into the downloaded JPEG. It includes smart auto-fill from coordinates via Nominatim and from pincode via Zippopotam.us. The result looks exactly like popular GPS camera Android apps but runs entirely in the browser with no backend required.