协作头脑风暴
brainstorming
在实现前澄清需求、探索设计选择,并形成清晰的开发方向。
maps-geolocation【开发与工程】Build location-based features with mapping APIs. Use when a user asks to integrate Google Maps, Mapbox, or Leaflet, implement geocoding and reverse geocoding, calculate routes and distances, build store locators, add interactive maps to web apps, implement geofencing, work with GeoJSON, build delivery tracking systems, optimize routes for fleets, dis…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Build location-based applications using mapping and geolocation APIs. This skill covers four major platforms — Google Maps Platform, Mapbox, Leaflet (open-source), and OpenStreetMap/Nominatim (free) — for geocoding, routing, interactive maps, geofencing, heatmaps, store locators, fleet tracking, and address autocomplete. Choose based on budget: Google Maps for full-featured commercial use, Mapbox for custom styling, Leaflet+OSM for zero-cost self-hosted solutions.
Google Maps Platform (billing account required; since March 2025 the flat $200 credit is replaced by a free monthly allowance per SKU, about 10,000 calls for Essentials SKUs such as Geocoding, 5,000 for Pro, 1,000 for Enterprise; check the pricing page for your SKUs):
# Enable in the Cloud console: Maps JavaScript API, Geocoding API, Routes API, Places API (New)
export GOOGLE_MAPS_API_KEY="AIzaSyD-example-restrict-this-key"
Directions API, Distance Matrix API and the old Places API are marked Legacy; use the Routes API (Step 4) for new projects. Restrict the key (HTTP referrers for browser keys, IP addresses or API restrictions for server keys).
Mapbox (free monthly tier: 50,000 GL JS map loads, 100,000 geocoding requests, 100,000 Directions requests; verify on the pricing page):
export MAPBOX_ACCESS_TOKEN="pk.eyJ1..."
Leaflet + OpenStreetMap (completely free, no API key):
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
Or npm install leaflet. Leaflet 1.9.4 is the stable release; 2.x is still alpha.
Nominatim (free geocoding, max 1 request/second, no key, but a User-Agent or Referer that identifies your app is mandatory; autocomplete-as-you-type and bulk geocoding are not allowed on the public server, and results must be cached. For heavy use self-host Nominatim or use a commercial geocoder). The OSM Foundation's policy (operations.osmfoundation.org/policies/nominatim) also says code generated by an LLM must follow it, so read it before shipping.
// Google Geocoding (v3 JSON endpoint; still served, see the v4 note below)
async function geocode(address: string) {
const res = await fetch(
`https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(address)}&key=${process.env.GOOGLE_MAPS_API_KEY}`
);
const data = await res.json();
// HTTP status is 200 even on errors: check the JSON status field
if (data.status !== "OK") throw new Error(`Geocoding failed: ${data.status}`);
const r = data.results[0];
return { lat: r.geometry.location.lat, lng: r.geometry.location.lng, formatted: r.formatted_address };
}
// Nominatim (free, no key; usage policy applies: identifying User-Agent, 1 request/second, cache results)
async function nominatimGeocode(query: string) {
const res = await fetch(
`https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(query)}&format=json&limit=5`,
{ headers: { "User-Agent": "StoreLocator/1.0 (https://brewline-coffee.dev)" } }
);
return res.json();
}
// Batch geocoding with rate limiting
async function batchGeocode(addresses: string[], delayMs = 100) {
const results = [];
for (const addr of addresses) {
try { results.push({ address: addr, ...await geocode(addr) }); }
catch (err) { results.push({ address: addr, lat: null, lng: null, error: err.message }); }
await new Promise(r => setTimeout(r, delayMs));
}
return results;
}
Geocoding API v4 is now generally available and is the successor to the v3 /maps/api/geocode/json endpoint. It is a REST API on geocode.googleapis.com: address geocoding is GET https://geocode.googleapis.com/v4/geocode/address/{address} with the key in the X-Goog-Api-Key header and an X-Goog-FieldMask header, place-id lookups use /v4/geocode/places/{place}, and there is a destinations search for building entrances. Parameters are renamed (language to languageCode, region to regionCode), the status field is gone (errors use HTTP status codes), and Google says v4 is server-to-server only, so call it from your backend, never from browser code. For new server-side projects use v4; existing v3 code keeps working.
Leaflet + OpenStreetMap (free):
const map = L.map("map").setView([48.8566, 2.3522], 12);
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
maxZoom: 19,
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
}).addTo(map);
L.marker([48.8584, 2.2945]).addTo(map)
.bindPopup("<b>Eiffel Tower</b><br>Paris, France").openPopup();
// Load GeoJSON layer
fetch("/data/zones.geojson").then(r => r.json()).then(data => {
L.geoJSON(data, {
style: { color: "#ef4444", weight: 2, fillOpacity: 0.1 },
onEachFeature: (feature, layer) => layer.bindPopup(feature.properties.name),
}).addTo(map);
});
The public OSM tile server is for light interactive use only (no bulk download or offline caching); for production traffic use a tile provider or self-host.
Mapbox GL JS (vector tiles, custom styles; current major is v3):
mapboxgl.accessToken = "pk.eyJ1...";
const map = new mapboxgl.Map({
container: "map", style: "mapbox://styles/mapbox/streets-v12",
center: [2.3522, 48.8566], zoom: 12,
});
new mapboxgl.Marker({ color: "#ef4444" })
.setLngLat([2.2945, 48.8584])
.setPopup(new mapboxgl.Popup().setHTML("<h3>Eiffel Tower</h3>"))
.addTo(map);
map.addControl(new mapboxgl.NavigationControl());
// Google Routes API (replaces the Directions and Distance Matrix APIs).
// POST + JSON body; the X-Goog-FieldMask header is mandatory and you pay for the fields you ask for.
const ROUTES = "https://routes.googleapis.com";
async function getRoute(origin: string, destination: string, travelMode = "DRIVE") {
const res = await fetch(`${ROUTES}/directions/v2:computeRoutes`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Goog-Api-Key": process.env.GOOGLE_MAPS_API_KEY!,
"X-Goog-FieldMask": "routes.distanceMeters,routes.duration,routes.polyline.encodedPolyline",
},
body: JSON.stringify({ origin: { address: origin }, destination: { address: destination }, travelMode }),
});
const route = (await res.json()).routes?.[0];
if (!route) throw new Error("No route found");
// duration is a string such as "1560s"
return { meters: route.distanceMeters, seconds: parseInt(route.duration), polyline: route.polyline.encodedPolyline };
}
// Many-to-many matrix: returns elements with originIndex, destinationIndex, distanceMeters, duration
async function distanceMatrix(origins: string[], destinations: string[]) {
const res = await fetch(`${ROUTES}/distanceMatrix/v2:computeRouteMatrix`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Goog-Api-Key": process.env.GOOGLE_MAPS_API_KEY!,
"X-Goog-FieldMask": "originIndex,destinationIndex,distanceMeters,duration,status,condition",
},
body: JSON.stringify({
origins: origins.map(address => ({ waypoint: { address } })),
destinations: destinations.map(address => ({ waypoint: { address } })),
travelMode: "DRIVE",
}),
});
return res.json();
}
// OSRM public demo server (free, no key, demo use only: self-host OSRM or use a hosted router in production).
// Coordinates are [lng, lat].
async function osrmRoute(coords: [number, number][]) {
const wp = coords.map(c => c.join(",")).join(";");
const res = await fetch(`https://router.project-osrm.org/route/v1/driving/${wp}?overview=full&geometries=geojson`);
const data = await res.json();
return { distance: data.routes[0].distance, duration: data.routes[0].duration, geometry: data.routes[0].geometry };
}
// Haversine distance (meters)
function haversineDistance(lat1: number, lng1: number, lat2: number, lng2: number): number {
const R = 6371000;
const dLat = ((lat2 - lat1) * Math.PI) / 180;
const dLng = ((lng2 - lng1) * Math.PI) / 180;
const a = Math.sin(dLat / 2) ** 2 +
Math.cos((lat1 * Math.PI) / 180) * Math.cos((lat2 * Math.PI) / 180) * Math.sin(dLng / 2) ** 2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
// Point-in-polygon check
function pointInPolygon(point: [number, number], polygon: [number, number][]): boolean {
const [x, y] = point;
let inside = false;
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
const [xi, yi] = polygon[i], [xj, yj] = polygon[j];
if ((yi > y) !== (yj > y) && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi) inside = !inside;
}
return inside;
}
// Circular geofence check
function isInRadius(point: [number, number], center: [number, number], radiusMeters: number): boolean {
return haversineDistance(point[0], point[1], center[0], center[1]) <= radiusMeters;
}
Nearby search with PostGIS:
SELECT id, name, address, lat, lng,
ST_DistanceSphere(ST_MakePoint(lng, lat), ST_MakePoint($1, $2)) AS distance_meters
FROM stores
WHERE ST_DWithin(ST_MakePoint(lng, lat)::geography, ST_MakePoint($1, $2)::geography, $3)
ORDER BY distance_meters LIMIT $4;
Route optimization (Routes API reorders intermediate stops; for large fleets with time windows and capacities use the Route Optimization API or an open-source solver such as VROOM):
async function optimizeRoute(origin: string, destination: string, stops: string[]) {
const res = await fetch("https://routes.googleapis.com/directions/v2:computeRoutes", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Goog-Api-Key": process.env.GOOGLE_MAPS_API_KEY!,
"X-Goog-FieldMask": "routes.optimizedIntermediateWaypointIndex,routes.distanceMeters,routes.duration",
},
body: JSON.stringify({
origin: { address: origin },
destination: { address: destination },
intermediates: stops.map(address => ({ address })),
travelMode: "DRIVE",
optimizeWaypointOrder: true,
}),
});
const route = (await res.json()).routes[0];
return {
optimizedOrder: route.optimizedIntermediateWaypointIndex, // indexes into stops
totalMeters: route.distanceMeters,
totalSeconds: parseInt(route.duration),
};
}
User prompt: "Build a store locator for our coffee shops. The user types an address, we geocode it, find the 5 nearest stores within 10km from our PostgreSQL database, and display them on a Leaflet map with distance labels."
The agent will create a geocoding function using Google Geocoding API (or Nominatim for low-volume, cached lookups within its usage policy) to convert the user's address input to coordinates. It will write a PostGIS SQL query using ST_DWithin to find the 5 nearest stores within 10,000 meters, returning name, address, coordinates, and distance. On the frontend, it will initialize a Leaflet map centered on the searched location, add a marker for each store with a popup showing name, address, and distance in km, and fit the map bounds to show all results.
User prompt: "Optimize the delivery route for a driver starting from our Berlin warehouse with 6 drop-off addresses, then set up a 200m geofence around each stop that logs arrival timestamps."
The agent will call the Google Routes API (computeRoutes with optimizeWaypointOrder: true) to reorder the 6 stops for minimum travel time, returning the optimal sequence, total distance, and estimated duration. It will then create a geofence monitor with a 200-meter circular zone around each stop using the haversine distance function, checking incoming GPS coordinates against each fence and logging enter/exit events with timestamps to track delivery progress.
{lat, lng} objects, Mapbox and GeoJSON use [lng, lat] arrays; mixing these up is the most common source of misplaced markers.google.maps.places.Autocomplete widget is Legacy; new code uses PlaceAutocompleteElement (await google.maps.importLibrary("places")) or Places API (New) autocomplete requests. Nominatim cannot be used for type-ahead.sk.) to the browser; browser code uses restricted keys or public pk. tokens only.