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.
- 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.
- The user should also be able to enter the city, state, country, pincode, and address manually using separate input fields for each.
- 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).
- A native
datetime-localpicker should be provided for date and time, pre-filled with the current date and time on page load. - 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.
- The entire app should be a single-page web app using plain HTML, CSS, and JavaScript — no frameworks.
A single-page web app with three sections:
- Input Section — Upload photo, enter lat/lng (with lookup button), city/state/country, pincode, address, and date/time
- Loading Section — Spinner while generating
- 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.
- 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 8765works fine)
index.html — Page markup + form
styles.css — Dark-theme styles
app.js — All application logic (~700 lines)
- Background:
#0f1117 - Card/input backgrounds:
#1a1d27 - Accent green:
#4CAF50(buttons, logo gradient) - Text:
#e0e0e0primary,#888secondary/labels - Borders:
#333inputs,#444buttons/upload area - Border radius:
14pxcards/upload,10pxinputs/buttons
- Max-width
800px, centered, with24px 16pxpadding - 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×56pxthumbnail, filename, and × clear button - Result section: Canvas preview with rounded corners, "Download Image" button (green, with download SVG icon) + "New Photo" button
-
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 viaalign-self:flex-end). Disabled until both lat/lng are valid numbers. Clicking it triggers reverse geocoding to auto-fill all fields below.
- Latitude (
-
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-size0.78rem. -
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")
- City (
-
Row 3 — Pincode + Address (2-column row):
- Pincode (
type="text",flex:0.4,maxlength="10", placeholder"Enter pincode") — wrapped in a.pincode-wrapperdiv 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")
- Pincode (
-
Date / Time — Native
<input type="datetime-local">withcolor-scheme: darkfor dark theme compatibility. Pre-filled with current local date/time on page load. -
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.
.form-rowand.form-row-3stack vertically below500px- Logo title shrinks to
1.3rem
- Manual: User clicks the magnifying glass lookup button (requires valid lat/lng).
- Automatic: When lat/lng
changeevents fire and the city field is still empty, a debounced lookup (800ms) triggers automatically. It does NOT overwrite user-entered data.
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
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, countryjoined with commas
- Lookup button icon changes to spinning
↻during request, reverts to 🔍 when done. - Green auto-fill notice shown for 4 seconds on success.
When the user types in the pincode field:
- Debounce
500msafter each keystroke. Ignore if fewer than 4 characters. - Show spinning
↻status indicator. - 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} - On first successful match: populate City (
place name), State (state), Country (country). Show✓(green). - If no country matches: Show
✗(red).
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.
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
┌──────────────────────────────────────────────────────────────────┐
│ (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│ │
│ └───────────────┘ └────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
- 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))
- 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):
- City, State, Country + flag image — bold white, font size
38 * S - Full address — light gray (
#d0d0d0), font size26 * S, word-wrapping supported - Lat/Long — monospace font (Consolas/Courier New),
#b8c4ce, font size26 * S, format:Lat 17.3840000° Long 78.4560000°(7 decimal places with degree symbol) - Date/Time —
#b8c4ce, font size24 * S, format:Friday, 27/02/2026 11.32 AM GMT +5.30
- City, State, Country + flag image — bold white, font size
- Floats above the info box (top-right corner)
- Dark semi-transparent rounded rectangle (
rgba(0,0,0,0.55), radius6*S) - Small green globe icon (circle + ellipse + horizontal line, stroked in
#4CAF50) - "GPS Map Camera" text in white bold, font size
16 * S
- 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
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.
- Convert lat/lng to tile coordinates at zoom 17 using Mercator projection
- Load a 3×3 grid of 256px tiles around the center tile
- Use Google satellite hybrid:
https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z} - Fallback to OSM:
https://tile.openstreetmap.org/{z}/{x}/{y}.pngif Google fails - Composite tiles on a 300×300 off-screen canvas, offset to center the exact coordinate
- Draw the red pin marker on top
- All tile images loaded with
crossOrigin = 'anonymous'
Native <input type="datetime-local"> with color-scheme: dark.
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);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
When the user clicks "Download", embed GPS coordinates into the JPEG EXIF data using piexif.js so the image is location-aware.
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]]
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.
- Upload area hides when file is selected, shows thumbnail instead
- Generate button stays disabled until a file is uploaded AND both lat/lng have valid numbers
- Lookup button stays disabled until both lat/lng have valid numbers
- Auto-fill from coordinates only triggers if city field is empty (won't overwrite user edits)
- Pincode lookup tries 16 countries sequentially and stops on first match
- "New Photo" returns to input section (preserves form data)
- Canvas output matches the original photo dimensions exactly
- All overlay elements scale proportionally to the image size via
S - Flag images loaded from CDN in parallel with map tiles (
Promise.all) - Address word-wrapping dynamically adjusts the info box height
- Both boxes share the same height (max of map vs info content)
- The form reads separate City, State, Country fields directly — no address string parsing needed
- If no address text is provided,
fullAddressfalls back tocity, state, countryjoined with commas
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.