Skip to content

dmgoldstein1/google-maps-timeline-viewer

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

114 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Google Maps Timeline Viewer

Enhanced with offline caching and Docker deployment

Generated by GitHub Copilot (GPT-5)

Google announced in 2024 that they would begin storing your Timeline data (ie your Location History) locally on your device, rather than in the cloud on their systems. The desktop version of their timeline viewer was also discontinued, since the data is now on your phone.

If you're like me, you have years worth of timeline data that you want to be able to view. This project enables that. It supports the old data format from Google Takeout, as well as the new on-device data format (which you can export from your device to your computer).

Features

Core Functionality

  • ✅ Supports Google Takeout, iOS Timeline, and Android Timeline formats
  • ✅ Interactive timeline and map view
  • ✅ Place details with icons and photos
  • ✅ Duration of visits and distance traveled (km or miles)
  • ✅ Color-coded travel modes with directional arrows
  • ✅ View multiple days at once
  • ✅ Search by place name
  • ✅ Find places within map area
  • ✅ Activity summary by year/month
  • ✅ Timezone selection for local times
  • ✅ Layer toggles (places, paths, etc.)
  • ✅ KML export
  • ✅ Mobile-friendly UI

Offline Caching System (New!)

  • 🚀 Dockerized deployment - One-command setup with Docker Compose
  • 💾 Persistent caching - SQLite database + filesystem storage
  • 📸 Optimized photos - Multi-resolution WebP and JPEG (150/400/800/1200px)
  • 🔄 Automatic prefetching - Download all place data in background
  • 📊 Quota management - Stay within Google Places API free tier (5000 requests/day)
  • 🌐 Offline support - Service Worker with intelligent caching strategies
  • 🔒 Secure API keys - Docker secrets for credential management
  • 🏗️ Multi-architecture - Supports amd64 and arm64 platforms

Screenshot


Screenshot 2


Table of Contents


Quick Start (Docker)

The fastest way to get started is with Docker:

Prerequisites

  • Docker and Docker Compose installed
  • Google Maps API key

1. Clone Repository

git clone https://github.com/kurupted/google-maps-timeline-viewer.git
cd google-maps-timeline-viewer

2. Configure API Key

# Create secrets directory
mkdir -p secrets

# Add your API key
echo "YOUR_GOOGLE_MAPS_API_KEY" > secrets/google_api_key.txt

3. Start Application

docker-compose up -d

4. Access Application

Open your browser to: http://localhost:8080

5. Upload Timeline Data

  1. Click the "📤 Upload" button in the timeline controls
  2. Select your Timeline.json file
  3. Wait for upload and format detection
  4. Click "⬇ Prefetch" to download all place details (optional but recommended)
  5. Select date range and click "Go" to view your timeline

That's it! Your data is cached locally and will work offline.


Manual Setup

If you prefer not to use Docker:

Prerequisites

  • Node.js 20 or higher
  • Google Maps API key

Installation (API only)

# Install dependencies
npm install

# Configure environment
cp .env.example .env
# Edit .env and add your API key

# Start API server (Express)
npm start

# Optional: run Caddy locally to serve static UI + proxy API
# Requires Caddy installed on your machine
# In a separate terminal:
caddy run --config Caddyfile

API runs on http://localhost:3000

When using Docker Compose, Caddy serves the UI at http://localhost:8080 and proxies /api to the API server. For manual local development, either run Caddy as above or use Docker for a fully integrated setup.


Architecture

The system consists of three main components:

┌─────────────┐      ┌──────────────┐      ┌─────────────────┐      ┌──────────────┐
│   Browser   │─────▶│    Caddy     │─────▶│   Node.js API   │─────▶│   SQLite DB  │
│             │◀─────│  Web Server  │◀─────│    (Express)    │◀─────│  + Photos    │
└─────────────┘      └──────────────┘      └─────────────────┘      └──────────────┘
      │                                              │
      │                                              │
      ▼                                              ▼
┌─────────────┐                              ┌─────────────────┐
│Service      │                              │ Google Places   │
│Worker Cache │                              │     API         │
└─────────────┘                              └─────────────────┘

Components:

  • Browser: Accesses the timeline viewer UI
  • Caddy: Reverse proxy serving static files and routing API requests
  • Node.js API: Handles place data caching, photo processing, and API quota management
  • SQLite + Photos: Persistent storage for cached data
  • Service Worker: Enables offline functionality
  • Google Places API: Source of place details and photos

API Endpoints

Cache Management

GET /api/cache/:placeId

Get cached place details.

Response (example):

{
  "place_id": "ChIJmQJIxlVYwokRLgeuocVOGVU",
  "displayName": "Times Square",
  "formattedAddress": "Manhattan, NY 10036, USA",
  "location": {"latitude": 40.758896, "longitude": -73.98513},
  "types": ["tourist_attraction", "point_of_interest"],
  "fromCache": true
}

Headers:

  • X-Cache-Status: stale when serving expired cache due to quota, or omitted when fresh
  • X-Quota-Reset: ISO 8601 timestamp when quota resets (present only when stale)

GET /api/photo/:placeId/:size

Get place photo at specified size.

Parameters:

  • size: One of 150, 400, 800, 1200 (pixels width)

Content Negotiation:

  • Serves WebP if Accept: image/webp header present
  • Falls back to JPEG otherwise

Prefetching

POST /api/prefetch

Start batch prefetch of place IDs.

Request:

{ "placeIds": ["ChIJ...", "ChIJ..."] }

Response:

{ "success": true, "total": 150, "alreadyCached": 42 }

GET /api/prefetch/progress

Server-Sent Events stream of prefetch progress.

Event data (example):

{
  "isRunning": true,
  "total": 150,
  "completed": 45,
  "currentPlace": "ChIJmQJIxlVYwokRLgeuocVOGVU",
  "errors": [],
  "startTime": 1732141200000,
  "estimatedTimeRemaining": 210
}

Timeline Management

POST /api/upload

Upload timeline JSON file.

Request: multipart/form-data with timeline file field

Response:

{
  "success": true,
  "fileId": 1,
  "filename": "Timeline.json",
  "format": "ios",
  "confidence": 95,
  "placeCount": 342,
  "message": "Detected iOS Timeline format",
  "requiresConfirmation": false
}

GET /api/timeline/list

List all uploaded timeline files.

Response:

[
  {
    "id": 1,
    "filename": "Timeline.json",
    "format": "ios",
    "confidence": 95,
    "uploadedAt": 1700000000,
    "active": true
  }
]

POST /api/timeline/select

Set active timeline file.

Request:

{
  "fileId": 1
}

Status

GET /api/stats

Get system statistics (API usage, cache counts, photos).

Response (example):

{
  "apiUsage": { "today": 234, "limit": 5000, "remaining": 4766, "quotaExceeded": false },
  "cache": { "placesCount": 342, "timelinesCount": 2 },
  "photos": { "total": 289, "bySize": {"150": 289, "400": 289} }
}

Configuration

Environment Variables

Variable Default Description
GOOGLE_MAPS_API_KEY - Google Maps API key (alternative to secrets)
GOOGLE_MAPS_API_KEY_FILE /run/secrets/google_api_key Path to API key file
CACHE_TTL_DAYS 180 Cache expiration in days (6 months)
DAILY_API_QUOTA 5000 Daily API request limit
MAX_CONCURRENT_FETCHES 5 Concurrent workers for prefetch
API_REQUEST_DELAY_MS 200 Delay between API requests (ms)
PORT 3000 API server port
NODE_ENV production Environment mode
LOG_LEVEL info Logging level (trace/debug/info/warn/error)
DATA_DIR /data Data storage directory
PHOTO_SIZES 150,400,800,1200 Photo sizes to generate (px)
WEBP_QUALITY 85 WebP compression quality (0-100)
JPEG_QUALITY 80 JPEG compression quality (0-100)

Testing

The repository uses Node’s built-in test runner.

npm test

What the tests cover:

  • API server basic liveness (/health)
  • Listing timeline files (/api/timeline/list)
  • Stats endpoint shape (/api/stats)

Tests start the API in a child process with an isolated temporary data directory to avoid touching your real cache.


Production Deployment

Docker Compose with Nginx

Create docker-compose.prod.yml:

version: '3.8'

services:
  timeline:
    image: ghcr.io/kurupted/google-maps-timeline-viewer:latest
    restart: unless-stopped
    volumes:
      - timeline-data:/data
      - timeline-files:/data/timeline
    secrets:
      - google_api_key
    environment:
      - NODE_ENV=production
      - LOG_LEVEL=warn
    networks:
      - timeline-network

  nginx:
    image: nginx:alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./ssl:/etc/nginx/ssl:ro
    networks:
      - timeline-network
    depends_on:
      - timeline

volumes:
  timeline-data:
  timeline-files:

secrets:
  google_api_key:
    file: ./secrets/google_api_key.txt

networks:
  timeline-network:

Nginx Configuration

Create nginx.conf:

events {
    worker_connections 1024;
}

http {
    upstream timeline {
        server timeline:8080;
    }

    # Rate limiting
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
    limit_req_zone $binary_remote_addr zone=upload:10m rate=1r/m;

    server {
        listen 80;
        server_name your-domain.com;

        # Redirect to HTTPS
        return 301 https://$server_name$request_uri;
    }

    server {
        listen 443 ssl http2;
        server_name your-domain.com;

        ssl_certificate /etc/nginx/ssl/fullchain.pem;
        ssl_certificate_key /etc/nginx/ssl/privkey.pem;

        # Security headers
        add_header X-Frame-Options "SAMEORIGIN" always;
        add_header X-Content-Type-Options "nosniff" always;
        add_header X-XSS-Protection "1; mode=block" always;

        location / {
            proxy_pass http://timeline;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location /api/ {
            limit_req zone=api burst=20 nodelay;
            proxy_pass http://timeline;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
        }

        location /api/upload {
            limit_req zone=upload burst=3;
            client_max_body_size 50M;
            proxy_pass http://timeline;
        }
    }
}

Let's Encrypt SSL Setup

# Install certbot
sudo apt install certbot

# Generate certificate
sudo certbot certonly --standalone -d your-domain.com

# Copy certificates
sudo cp /etc/letsencrypt/live/your-domain.com/fullchain.pem ./ssl/
sudo cp /etc/letsencrypt/live/your-domain.com/privkey.pem ./ssl/
sudo chmod 644 ./ssl/*.pem

Volume Management

Backup

# Backup all data
docker run --rm \
  -v google-maps-timeline-viewer_timeline-data:/data \
  -v $(pwd):/backup \
  ubuntu tar czf /backup/timeline-backup-$(date +%Y%m%d).tar.gz /data

# Backup timeline files only
docker run --rm \
  -v google-maps-timeline-viewer_timeline-files:/data/timeline \
  -v $(pwd):/backup \
  ubuntu tar czf /backup/timeline-files-$(date +%Y%m%d).tar.gz /data/timeline

Restore

# Restore data
docker run --rm \
  -v google-maps-timeline-viewer_timeline-data:/data \
  -v $(pwd):/backup \
  ubuntu tar xzf /backup/timeline-backup-20251121.tar.gz -C /

# Verify
docker-compose exec app ls -lh /data

Clear Cache

# Stop services
docker-compose down

# Remove data volume (WARNING: deletes all cached data)
docker volume rm google-maps-timeline-viewer_timeline-data

# Restart
docker-compose up -d

Troubleshooting

Quota Exceeded

Symptom: UI shows "Using stale data" warning

Solution:

  1. Check quota usage: curl http://localhost:8080/api/status
  2. Wait for quota reset (shown in warning)
  3. Increase DAILY_API_QUOTA if needed
  4. Stale data is still functional, just not refreshed

Format Detection Failed

Symptom: Upload shows low confidence (<50%)

Solution:

  1. Verify JSON file is valid
  2. Check format matches supported types (iOS, Google Takeout, Semantic Segments)
  3. Override format in UI if detection is incorrect
  4. Check logs: docker-compose logs app | grep format

Photos Not Loading

Symptom: Images show broken or don't appear

Solution:

  1. Check photo storage: docker-compose exec app ls -lh /data/photos
  2. Verify photo processing: docker-compose logs app | grep photo
  3. Try re-fetching place: DELETE and re-fetch via prefetch
  4. Check disk space: docker-compose exec app df -h /data

High Memory Usage

Symptom: Container uses >2GB RAM

Solution:

  1. Reduce concurrent fetches: Set MAX_CONCURRENT_FETCHES=3
  2. Lower photo quality: Set WEBP_QUALITY=75 JPEG_QUALITY=70
  3. Limit photo sizes: Set PHOTO_SIZES=150,400
  4. Increase Docker memory limit in docker-compose.yml

Database Locked

Symptom: "database is locked" errors

Solution:

  1. Stop concurrent prefetch operations
  2. Check for stuck processes: docker-compose exec app ps aux
  3. Restart container: docker-compose restart app
  4. Database uses WAL mode for better concurrency

Logs

# View all logs
docker-compose logs -f

# View app logs only
docker-compose logs -f app

# View specific log files
docker-compose exec app tail -f /data/logs/prefetch.log
docker-compose exec app tail -f /data/logs/caddy.log
docker-compose exec app tail -f /data/logs/access.log

# Export logs
docker-compose logs --no-color > timeline-logs-$(date +%Y%m%d).txt

Contributing

Local Development

# Clone repository
git clone https://github.com/kurupted/google-maps-timeline-viewer.git
cd google-maps-timeline-viewer

# Install dependencies
npm install

# Set up environment
cp .env.example .env
# Edit .env with your API key

# Create data directories
mkdir -p data/photos data/timeline data/logs

# Start development server (with auto-reload)
npm run dev

# In another terminal, start Caddy (or use any web server)
caddy file-server --root ./public --listen :8080

Running Tests

# Run all tests
npm test

# Run specific test file
node --test server/db.test.js

# Run with coverage
npm test -- --coverage

Code Style

  • Use ES modules (import/export)
  • Follow existing formatting
  • Add JSDoc comments for functions
  • Include Copilot generation header
  • No emojis in code (per project guidelines)

Submitting Pull Requests

  1. Fork the repository
  2. Create a feature branch: git checkout -b feature/my-feature
  3. Make your changes
  4. Test thoroughly
  5. Commit with descriptive message
  6. Push and create pull request
  7. Ensure CI passes (tests, security scan)

License

MIT License - see LICENSE file for details


Credits

Original project by kurupted
Dockerization and API caching system generated by GitHub Copilot using Claude Sonnet 4.5

Get Your Data: Option 2, Google Takeout (No longer available once you've started using on-device data)

  1. Go to Google Takeout:

  2. Select Data to Include:

    • Click "Deselect all".
    • Scroll down and select Timeline. (The default format is JSON, which is what we want.)
    • Click "Next step".
  3. Customize Export Format:

    • Choose the delivery method, etc. Doesn't matter what you choose here.
    • Click "Create export".
  4. Download the Export:

    • Once the export is ready, download the file.
    • Extract the archive to anywhere you like, eg My Documents.
    • Ensure that the archive contained a folder called "Location History (Timeline)". If not, you'll need to use on-device data, as described above. (Note that "Timeline Edits.json" is not the same as the "Timeline.json" that comes from your device.)

Obtain a Google Maps API Key

  1. Go to the Google Cloud Console:

  2. Create a New Project:

    • Click on the project drop-down and select "New Project".
    • Enter a name for your project and click "Create".
  3. Enable APIs:

    • In the Cloud Console, go to APIs & Services.
    • Search for the following APIs and enable them:
      • Maps JavaScript API
      • Maps Embed API
      • Places API (New)
    • You'll need to enable Billing to use the APIs. With normal use, you shouldn't incur any charges. See bottom of this doc for more info.
  4. Create API Key:

    • Go to APIs & Services > Credentials.
    • Click "Create Credentials" and select "API Key".
    • Copy the generated API key.
  5. Restrict API Key (Optional):

    • In the API key's settings, under API restrictions, select "Restrict key" and choose the APIs you enabled.

Final Set Up

  1. Download The Google Maps Timeline Viewer:

    • Save this project's timeline.html file anywhere you like, either on your computer (eg in My Documents) or on your mobile device. (To save, copy/paste the text into eg Notepad, and save as "timeline.html")
  2. Add your API key:

    • Open the timeline.html file in a text editor and find the code below, near the top:
      <script>
           window.GOOGLE_MAPS_API_KEY = "YOUR_API_KEY"; // Replace YOUR_API_KEY with your actual key
    • Replace YOUR_API_KEY with the key you obtained from the Google Cloud Console, and save.

View Your Timeline

  1. Open the timeline.html File:
    • Open the file on your computer or mobile device. Supported browsers include Chrome and Microsoft Edge. Firefox will unfortunately not work.
    • At the top left of the page, click Load Data and navigate to the folder that contains your Timeline data.
      • For Google Takeout data: The folder structure should be "Takeout\Location History (Timeline)\Semantic Location History". Once you are within the "Semantic Location History" folder, and see subfolders for each year, click "Select Folder" on the dialog. (Do not navigate into one of the year folders.)
      • For On-Device exported data: Simply choose the folder that contains your exported Timeline.json file.
    • Note that on Android you may not be allowed to load the data if it's in certain folders, such as Downloads -- move the data file to an accessible location.

Note on API Usage & Billing

  • You can find the free usage limits, and over-the-limit pricing here: https://developers.google.com/maps/billing-and-pricing/pricing#places-pricing
  • Depending on what information is requested during the API call, it will fall into tiers, eg Essentials or Pro. Details here: https://developers.google.com/maps/billing-and-pricing/sku-details#place-details-pro-sku
  • At this time, the monthly free limits are 1,000 Place Photos, 5,000 Pro-level requests, and 10,000 Essentials-level requests.
  • There is also an option within the Timeline Viewer to disable API calls, which will still show your activity paths and place visits, however all of the places will be labeled "Unknown Location".
  • You can also save retrieved Place data to a file, which will be loaded automatically next time, to avoid API calls that were done previously.

About

View your Google Maps Timeline (Location History) data

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages

  • HTML 73.8%
  • JavaScript 23.9%
  • Shell 1.6%
  • Dockerfile 0.7%