Build a Minimal Satellite‑View Map Clone

A beginner‑friendly UVF IT guide to a live globe with public data

Welcome to another official UVF IT build guide! In the next 45 minutes you’ll spin up a tiny version of the God’s Eye View globe—showing live aircraft and ship positions on a 3‑D map—using only free, open‑source tools.

We’ll use Node.js, Express, SQLite, and the public OpenSky and MarineTraffic APIs (no keys required for basic endpoints). The result runs locally in your browser and can be extended with more layers later.

Before you start…

Minimal Clone Architecture
LOCAL RUNTIME fetch JSON store/retrieve Node.js Express Server SQLite DB (cache) HTML + Mapbox GL JS
1

Phase 1: Scaffold the project

Create a folder for the clone, initialise a fresh npm project, and pull in the minimal runtime libraries we need: Express for the HTTP server, better‑sqlite3 for a tiny embedded cache, and node‑fetch to call the public APIs.

These steps give us a clean starting point and a predictable dependency tree, so later code can focus on data handling and serving JSON to the front‑end.

mkdir gods-eye-clone && cd gods-eye-clone
npm init -y
npm install express better-sqlite3 node-fetch
2

Phase 2: Data model & caching

We define a simple SQLite table that stores the last known latitude, longitude, timestamp, and a type flag (aircraft or ship). Then we write a small Node script that pulls the ten most recent positions from OpenSky (for aircraft) and MarineTraffic (for ships) and writes them into the cache.

Running this script populates the database so the Express server can later serve a static JSON feed to the map.

fetch_data.js

const fetch = require('node-fetch');
const Database = require('better-sqlite3');
const db = new Database('cache.db');
// Ensure table exists
db.exec(`CREATE TABLE IF NOT EXISTS positions (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  type TEXT NOT NULL,
  lat REAL NOT NULL,
  lon REAL NOT NULL,
  ts INTEGER NOT NULL
);`);
async function fetchOpenSky(){
  const resp = await fetch('https://opensky-network.org/api/states/all');
  const data = await resp.json();
  return data.states.slice(0,10).map(s=>({type:'aircraft',lat:s[6],lon:s[5],ts:s[4]}));
}
async function fetchMarineTraffic(){
  const resp = await fetch('https://services.marinetraffic.com/api/exportvesseltrack/v:2/username:demo/period:10');
  const text = await resp.text();
  // Simple CSV parse – assume lat,lon,timestamp are columns 2,3,4
  return text.split('\n').slice(1,11).map(line=>{
    const cols = line.split(',');
    return {type:'ship',lat:parseFloat(cols[2]),lon:parseFloat(cols[3]),ts:parseInt(cols[4])};
  });
}
async function cache(){
  const all = [...await fetchOpenSky(), ...await fetchMarineTraffic()];
  const stmt = db.prepare('INSERT INTO positions (type,lat,lon,ts) VALUES (?,?,?,?)');
  const insert = db.transaction(rows=>{for(const r of rows) stmt.run(r.type,r.lat,r.lon,r.ts);});
  insert(all);
  console.log('Cached',all.length,'records');
}
cache().catch(e=>console.error(e));
3

Phase 3: Simple API & UI server

In this step we spin up a tiny Express server that does two things: it serves a static HTML page and it exposes a /positions endpoint. The endpoint reads the cached CSV‑to‑GeoJSON file we created earlier and returns it as a proper GeoJSON FeatureCollection, so the front‑end can plot the points directly. Keeping the server minimal makes it easy to run on any laptop without extra configuration.

server.js

const express = require('express');
const path = require('path');
const fs = require('fs');

const app = express();
const PORT = process.env.PORT || 3000;

// Serve static files from the "public" folder (our UI)
app.use(express.static(path.join(__dirname, 'public')));

// Helper to load cached GeoJSON (generated by fetch_data.js)
function loadGeoJSON() {
  const filePath = path.join(__dirname, 'data', 'positions.geojson');
  if (fs.existsSync(filePath)) {
    return JSON.parse(fs.readFileSync(filePath, 'utf8'));
  }
  // Fallback empty collection if the file is missing
  return { type: 'FeatureCollection', features: [] };
}

// API endpoint that returns the cached positions as GeoJSON
app.get('/positions', (req, res) => {
  const geojson = loadGeoJSON();
  res.json(geojson);
});

app.listen(PORT, () => {
  console.log(`🚀 http://localhost:${PORT}`);
});
4

Phase 4: Run locally

Now we fire up the data fetcher once so the server has something to serve, then start the Express server. All commands run in a terminal from the project root. Once the server is listening, opening http://localhost:3000 in a browser will load the map UI we just created.

# 1️⃣ Pull the latest satellite positions (runs once and caches the result)
node fetch_data.js

# 2️⃣ Start the API + UI server
node server.js
5

Phase 5: Verify & extend

With the server running, open the URL in a browser. You should see a world map with red and blue dots representing the satellite positions we cached. The /positions endpoint can be inspected with curl to confirm it returns valid GeoJSON. From here you can easily add more layers – for example, pull an earthquake feed, transform it to GeoJSON, and merge it into positions.geojson before the server serves it.

public/index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Minimal Satellite‑View Clone</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <script src="https://api.mapbox.com/mapbox-gl-js/v2.15.0/mapbox-gl.js"></script>
  <link href="https://api.mapbox.com/mapbox-gl-js/v2.15.0/mapbox-gl.css" rel="stylesheet" />
  <style>
    body, html { margin:0; padding:0; height:100%; }
    #map { width:100%; height:100%; }
  </style>
</head>
<body>
<div id="map"></div>
<script>
  // 👉 Replace with your own public token (you can get one for free at mapbox.com)
  mapboxgl.accessToken = 'YOUR_MAPBOX_PUBLIC_TOKEN';

  const map = new mapboxgl.Map({
    container: 'map',
    style: 'mapbox://styles/mapbox/streets-v12',
    center: [0, 20],
    zoom: 1.5
  });

  // Load the GeoJSON from our API and add it as a source/layer
  fetch('/positions')
    .then(res => res.json())
    .then(data => {
      map.on('load', () => {
        map.addSource('satellites', { type: 'geojson', data });
        map.addLayer({
          id: 'satellite-points',
          type: 'circle',
          source: 'satellites',
          paint: {
            'circle-radius': 4,
            'circle-color': ['get', 'color'], // expects a "color" property per feature
            'circle-stroke-width': 1,
            'circle-stroke-color': '#fff'
          }
        });
      });
    })
    .catch(err => console.error('Failed to load positions:', err));
</script>
</body>
</html>

Verifying the Stack

CheckCommand / Why it matters
Server starts without errornode server.js → should log ‘🚀 http://localhost:3000’
/positions returns valid GeoJSONcurl http://localhost:3000/positions | jq '.type' → should output "FeatureCollection"
Browser shows map with red and blue dotsOpen http://localhost:3000 in a browser; you should see points plotted on the world map

What’s next?

You now have a functional clone. Add voice control with Web Speech API, swap Mapbox for free OpenStreetMap tiles, or pull additional public feeds (earthquakes, weather). Happy hacking!