Instrukcja dla agentów AI: podłączenie danych do portalu MapaDanych
Ten dokument jest przeznaczony dla agentów AI, którzy przygotowują dane dla portalu MapaDanych. Dostawcą może być każdy, kto ma dane z lokalizacją. Opisuje cel, procedurę, zasady i kryteria ukończenia pracy. Człowiek, który kieruje agentem, znajdzie przystępniejszą wersję w przewodniku dla dostawców.
Cel
Opublikować przez HTTPS:
- manifest – JSON zgodny ze schematem manifestu,
- dla każdego zbioru danych źródło danych – plik GeoJSON lub endpoint zgodny ze schematem danych i kontraktem OpenAPI dostawcy, ewentualnie istniejące kafle rastrowe / WMS,
tak, aby walidator zwrócił "valid": true i jak najmniej ostrzeżeń.
Co przeczytać
| Źródło | Dlaczego |
|---|---|
| llms.txt | spis całej dokumentacji |
| Protokół 0.1 | specyfikacja normatywna – w razie wątpliwości obowiązuje ona |
| JSON Schema manifestu, danych | dokładne typy, limity i opisy pól |
| OpenAPI dostawcy | kontrakt endpointu, można z niego wygenerować serwer |
| manifest-complete.json, manifest-minimal.json | poprawne wzory |
| data-points.json, data-polygons.json | poprawne wzory odpowiedzi |
| Katalog | co już jest w portalu – nie twórz duplikatów, inspiruj się słowami kluczowymi |
Jeśli potrafisz korzystać z MCP, połącz się z serwerem https://mapadanych.pl/mcp (Streamable HTTP, bez uwierzytelniania). Narzędzia documentation, schema, example, validate_manifest, validate_data i catalog obejmują wszystko, co jest w tej instrukcji. Zob. serwer MCP.
Procedura
1. Inwentaryzacja danych
Ustal, jakie dane przestrzenne ma dostawca: bazy danych (PostGIS, MS SQL), pliki GIS (SHP, GPKG, KML, CSV ze współrzędnymi), istniejące API, usługi WMS/WMTS. Dla każdego źródła ustal:
- typ geometrii (punkty / linie / powierzchnie),
- układ współrzędnych (w Polsce często PUWG 1992, EPSG:2180 – trzeba przeliczyć),
- liczbę obiektów i jak często się zmieniają,
- licencję i kto może ją zatwierdzić,
- czy zawiera dane osobowe (imiona i nazwiska, numery PESEL, adresy osób fizycznych, numery rejestracyjne pojazdów…).
2. Projekt zbiorów danych
- Jeden zbiór = jeden rodzaj obiektów z jednym sposobem wyświetlania. Kontenery i mapę hałasu umieść w dwóch zbiorach.
idzbioru i dostawcy: małe litery bez znaków diakrytycznych, cyfry, łączniki (kontenery-na-odpady). Po publikacji go nie zmieniaj – jest częścią odnośników.- Wybór źródła:
| Sytuacja | source |
|---|---|
| do ~2 000 obiektów, zmieniają się najwyżej raz dziennie | plik statyczny, "type": "geojson", "mode": "full" |
| więcej obiektów lub dane na żywo | endpoint, "mode": "bbox", ustaw coverage.bbox, a przy dużych zbiorach coverage.minZoom |
| istnieją już kafle WMS / XYZ | "type": "raster" z szablonem {bbox-epsg-3857} lub {z}/{x}/{y}; uzupełnij presentation.legend (kolory i etykiety z legendy usługi, np. ArcGIS …/MapServer/legend?f=json lub WMS GetLegendGraphic) |
3. Konwersja danych
- Współrzędne WGS84 (EPSG:4326), kolejność
[lon, lat]. Dla Polski to mniej więcej[19, 52], a nie[52, 19]. - Zaokrąglij do 6 miejsc po przecinku (≈ 10 cm), reszta tylko powiększa odpowiedź.
- Wielokąty muszą mieć zamknięte pierścienie (pierwszy punkt = ostatni).
- Każdy obiekt powinien mieć stabilne
id. - Właściwości (
properties) jako proste wartości – ciągi znaków, liczby, wartości logiczne, daty w ISO 8601. Żadnego HTML. - Liczby jako liczby (
1100), nie ciągi znaków ("1100 l"); jednostkę umieść wfields[].unit.
Konwersja pliku GIS do GeoJSON:
ogr2ogr -f GeoJSON -t_srs EPSG:4326 -lco RFC7946=YES -lco COORDINATE_PRECISION=6 lavicky.geojson vstup.shp
Przeliczenie współrzędnych w Pythonie:
from pyproj import Transformer
to_wgs84 = Transformer.from_crs("EPSG:2180", "EPSG:4326", always_xy=True)
lon, lat = to_wgs84.transform(x_puwg, y_puwg) # always_xy=True zwraca [lon, lat]
4. Manifest
Wypełnij nie tylko pola obowiązkowe, ale też te, od których zależy, czy ktoś dane znajdzie:
description– 1–2 zdania: co zawierają dane, skąd pochodzą, jak są aktualne.keywords– 5–15 słów i synonimów po polsku, łącznie z formami potocznymi (śmietnikobokkontener).useCases– w czym dane pomagają:construction,housing,land-purchase,agriculture,investment,transport,tourism,business,public-administration. Według nich asystent portalu dobiera dane do polecenia typu „chcę budować w gminie X”.category– jedna z:territory,nature,water,risks,energy,transport,society,economy,infrastructure.fields– każda właściwość, którą ma widzieć użytkownik:label,type,unit. Jedną oznacz jakoprimary(nagłówek w szczegółach).license– nazwa i tekst atrybucji. Nie wymyślaj licencji; jeśli jej nie znasz, zapytaj człowieka.temporal.updated,temporal.frequency– aktualność danych.temporal.param– dla danych w czasie (wypadki, pomiary, zdarzenia): klucz parametru typurangez latami (min/max= pierwszy i ostatni rok), którym użytkownik wybiera okres. Odpowiedź powinna wtedy zawieraćdm.timelinez liczbami według lat (przy wartościach w czasie, takich jak liczba ludności, zamiast tego zbiorczydm.chart), zob. filtr czasowy.live– tylko dla danych zmieniających się w minutach (pozycje pojazdów, dostępność rowerów):{ "interval": 60 }w sekundach, 10 do 3600. Otwarta mapa sama wtedy odświeża zbiór. Ustaw co najmniejcache.ttl, inaczej walidator zgłosilive_interval_below_ttl.- Zmiany w czasie – sprawdź, czy obiekty mają wartość za wiele okresów (stany według lat, pomiary, ceny). Jeśli tak, wysyłaj ją zawsze przy każdym obiekcie w
dm.chart; jeśli sensem zbioru jest zmiana, koloruj według trendu. Zob. zmiany w czasie i trend.
5. Wizualizacja
| Geometria | Odpowiedni typ | Uwaga |
|---|---|---|
| punkty, dziesiątki do setek | circle |
kolor według kategorii (categorical), etykieta przez label |
| punkty, tysiące | circle z cluster lub heatmap |
mapa ciepła tylko dla gęstości, nie pokazuje pojedynczych obiektów |
| linie | line |
szerokość może zależeć od wartości |
| powierzchnie z wartością | fill + interpolate lub step |
kartogram; opacity 0.4–0.6, aby było widać podkład |
| punkty lub powierzchnie, których wartość zmienia się w czasie | circle / fill + color: { "type": "trend" } |
gdy chodzi o zmianę (rośnie / maleje); szereg wartości w dm.chart przy każdym obiekcie, trend oblicza portal |
| powierzchnie bez wartości | fill z jednym kolorem |
Zasady: kolory hex; kategorie maks. ~8 kolorów; dla wartości użyj skali stopniowej od jasnej do ciemnej; field w prezentacji musi odpowiadać właściwości w danych (i w fields, z typem liczbowym przy interpolate/step). Jeśli wartość obiektu ma historię, wyślij ją w dm.chart przy dowolnej prezentacji – także przy kartogramie z wyborem roku (wybrany rok w highlight).
6. Endpoint (tylko przy mode: "bbox" lub danych na żywo)
Zaimplementuj GET zgodnie z kontraktem OpenAPI:
- query
bbox=minLon,minLat,maxLon,maxLat– zwróć obiekty przecinające wycinek, zoom– liczba całkowita; przy małym przybliżeniu możesz zwracać agregacje,limit– nie zwracaj więcej obiektów; jeśli je obetniesz, ustawdm.truncated: trueidm.total,- własne parametry z
params(zakresod,do, wiele wartościa,b,c), Content-Type: application/geo+jsonlubapplication/json, HTTPS, odpowiedź w ciągu 1 s,- nieznane parametry query ignoruj (
lang,protocol,cursormożesz pominąć).
CORS nie jest potrzebny – portal wysyła zapytania z serwera.
Python (FastAPI + PostGIS):
from fastapi import FastAPI
app = FastAPI()
@app.get("/datovamapa/kontejnery")
def kontejnery(bbox: str | None = None, zoom: int | None = None, limit: int = 5000, druh: str | None = None):
west, south, east, north = map(float, bbox.split(",")) if bbox else (-180, -90, 180, 90)
druhy = druh.split(",") if druh else None
rows = db.fetch_all(
"""
SELECT id, druh, objem, vyvoz::text, ST_AsGeoJSON(geom, 6)::json AS geometry
FROM kontejnery
WHERE geom && ST_MakeEnvelope(%s, %s, %s, %s, 4326)
AND (%s::text[] IS NULL OR druh = ANY(%s))
LIMIT %s
""",
(west, south, east, north, druhy, druhy, limit + 1),
)
features = [
{"type": "Feature", "id": r["id"], "geometry": r["geometry"],
"properties": {"druh": r["druh"], "objem": r["objem"], "vyvoz": r["vyvoz"]}}
for r in rows[:limit]
]
return {"type": "FeatureCollection", "features": features, "dm": {"truncated": len(rows) > limit, "ttl": 3600}}
TypeScript (Cloudflare Worker / Node, dane w pamięci):
// KONTEJNERY: tablica GeoJSON Feature z punktami, wczytana przy starcie (z bazy danych lub pliku).
declare const KONTEJNERY: { type: "Feature"; id: number; geometry: { type: "Point"; coordinates: [number, number] }; properties: Record<string, unknown> }[];
export default {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
const [west, south, east, north] = (url.searchParams.get("bbox") ?? "-180,-90,180,90").split(",").map(Number);
const limit = Math.min(Number(url.searchParams.get("limit")) || 5000, 5000);
const inside = KONTEJNERY.filter(({ geometry: { coordinates: [lon, lat] } }) =>
lon >= west && lon <= east && lat >= south && lat <= north);
return Response.json(
{ type: "FeatureCollection", features: inside.slice(0, limit), dm: { total: inside.length, truncated: inside.length > limit } },
{ headers: { "Content-Type": "application/geo+json", "Cache-Control": "public, max-age=300" } },
);
},
};
7. Walidacja
Powtarzaj, dopóki są błędy, i przejrzyj każde ostrzeżenie (napraw je albo świadomie pozostaw i uzasadnij):
# manifest (bez publikacji)
curl -sX POST https://mapadanych.pl/api/validate -H 'Content-Type: application/json' --data @manifest.json
# dane względem konkretnego zbioru
curl -sX POST https://mapadanych.pl/api/validate -H 'Content-Type: application/json' \
--data "{\"data\": $(cat lavicky.geojson), \"dataset\": $(jq '.datasets[0]' manifest.json)}"
# test na żywo opublikowanego manifestu i wszystkich źródeł
curl -s 'https://mapadanych.pl/api/validate?manifestUrl=https://www.gmina-przyklad.pl/datovamapa/manifest.json'
Odpowiedź: { valid, summary, errors: [{ path, code, message, hint? }], warnings: [...] }, przy teście na żywo dodatkowo datasets[] z zapytaniem, czasem odpowiedzi i liczbą obiektów. Znaczenie kodów jest opisane w API portalu.
8. Przekazanie człowiekowi
Na koniec podsumuj:
- URL manifestu i listę zbiorów (id, nazwa, typ źródła, liczba obiektów),
- wynik testu na żywo (
summary, pozostałe ostrzeżenia i dlaczego zostały), - co musi potwierdzić człowiek: licencję i atrybucję, że dane nie zawierają danych osobowych, kontakt dla portalu,
- jak aktualizować dane (skrypt, cron, źródło prawdy).
Gdy człowiek potwierdzi licencję i treść, zarejestruj manifest: POST https://mapadanych.pl/api/registration z { "manifestUrl": "…" } lub narzędziem MCP register_provider. Dane będą dostępne jako niezatwierdzone (z ostrzeżeniem, poza katalogiem), dopóki operator ich nie zatwierdzi. Po każdej zmianie manifestu czekają na ponowne zatwierdzenie. Szczegóły: rejestracja.
Zasady
- Nie publikuj danych osobowych (RODO). W razie wątpliwości dane zagreguj lub pomiń i zapytaj.
- Nie wymyślaj licencji, źródła ani aktualności danych.
- Nie wstawiaj HTML do
propertiesanidm; portal wyświetla wszystko jako tekst. - Nie umieszczaj tajnych kluczy w URL – manifest jest publiczny.
- Nie zmieniaj
provider.idanidataset.idpo publikacji. - Nie używaj kluczy
bbox,zoom,limit,cursor,lang,protocoldla własnych parametrów. - Maks. 5 000 obiektów i 15 MB na odpowiedź, odpowiedź w ciągu 10 s (celem jest 1 s).
Kryteria ukończenia pracy
- Manifest jest dostępny przez HTTPS, a test na żywo walidatora zwraca
"valid": true. - Każdy zbiór ma
description,keywords,useCases,categoryi (przy GeoJSON)fields. - Licencja i atrybucja są potwierdzone przez człowieka.
- Współrzędne są w WGS84
[lon, lat], brak ostrzeżeńswapped_coordinatesioutside_coverage. - Żaden zbiór nie zwraca w teście 0 obiektów (
empty_response), chyba że jest to zamierzone. - Obiekty mające wartość za wiele okresów wysyłają
dm.chart(brakmissing_trend_chartitrend_without_chart). - Czas odpowiedzi endpointów jest poniżej 3 s (
slow_response). - Dane nie zawierają danych osobowych.
- Opisano, jak dane są aktualizowane.
Polecenie dla agenta (do skopiowania)
Człowiek może dać swojemu agentowi ten tekst:
Przygotuj nasze dane przestrzenne dla portalu MapaDanych (https://mapadanych.pl).
Postępuj zgodnie z instrukcją https://mapadanych.pl/dokumentacja/dla-agentow-ai.md
(spis dokumentacji: https://mapadanych.pl/llms.txt, serwer MCP: https://mapadanych.pl/mcp).
Źródła danych: <opisz bazy danych, pliki, usługi>.
Gdzie opublikować: <strona / serwer / repozytorium>.
Licencja: <np. CC BY 4.0, atrybucja „© Gmina Przykład”> – innej nie wymyślaj.
Gotowe, gdy test na żywo walidatora zwraca valid: true i spełnisz kryteria ukończenia pracy.
Na koniec przekaż mi podsumowanie zgodnie z krokiem 8.