# Instrukcja dla agentów AI: podłączenie danych do portalu MapaDanych {#navod-pro-ai-agenty-pripojeni-dat-do-datovemapy}

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](https://mapadanych.pl/dokumentacja/dla-dostawcow.md).

## Cel {#cil}

Opublikować przez HTTPS:

1. **manifest** – JSON zgodny ze [schematem manifestu](https://mapadanych.pl/schema/0.1/manifest.json),
2. dla każdego zbioru danych **źródło danych** – plik GeoJSON lub endpoint zgodny ze [schematem danych](https://mapadanych.pl/schema/0.1/data.json) i [kontraktem OpenAPI dostawcy](https://mapadanych.pl/schema/0.1/provider-openapi.json), ewentualnie istniejące kafle rastrowe / WMS,

tak, aby [walidator](https://mapadanych.pl/dokumentacja/api.md#validator) zwrócił `"valid": true` i jak najmniej ostrzeżeń.

## Co przeczytać {#co-si-nacist}

| Źródło | Dlaczego |
|---|---|
| [llms.txt](https://mapadanych.pl/llms.txt) | spis całej dokumentacji |
| [Protokół 0.1](https://mapadanych.pl/dokumentacja/protokol.md) | specyfikacja normatywna – w razie wątpliwości obowiązuje ona |
| [JSON Schema manifestu](https://mapadanych.pl/schema/0.1/manifest.json), [danych](https://mapadanych.pl/schema/0.1/data.json) | dokładne typy, limity i opisy pól |
| [OpenAPI dostawcy](https://mapadanych.pl/schema/0.1/provider-openapi.json) | kontrakt endpointu, można z niego wygenerować serwer |
| [manifest-complete.json](https://mapadanych.pl/przyklady/manifest-complete.json), [manifest-minimal.json](https://mapadanych.pl/przyklady/manifest-minimal.json) | poprawne wzory |
| [data-points.json](https://mapadanych.pl/przyklady/data-points.json), [data-polygons.json](https://mapadanych.pl/przyklady/data-polygons.json) | poprawne wzory odpowiedzi |
| [Katalog](https://mapadanych.pl/zrodla.md) | 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](https://mapadanych.pl/dokumentacja/api.md#mcp-server).

## Procedura {#postup}

### 1. Inwentaryzacja danych {#1-inventura-dat}

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 {#2-navrh-datovych-sad}

- Jeden zbiór = jeden rodzaj obiektów z jednym sposobem wyświetlania. Kontenery i mapę hałasu umieść w dwóch zbiorach.
- `id` zbioru 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 {#3-prevod-dat}

- 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ść w `fields[].unit`.

Konwersja pliku GIS do GeoJSON:

```bash
ogr2ogr -f GeoJSON -t_srs EPSG:4326 -lco RFC7946=YES -lco COORDINATE_PRECISION=6 lavicky.geojson vstup.shp
```

Przeliczenie współrzędnych w Pythonie:

```python
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 {#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 (`śmietnik` obok `kontener`).
- `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 jako `primary` (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 typu `range` z latami (`min`/`max` = pierwszy i ostatni rok), którym użytkownik wybiera okres. Odpowiedź powinna wtedy zawierać `dm.timeline` z liczbami według lat (przy wartościach w czasie, takich jak liczba ludności, zamiast tego zbiorczy `dm.chart`), zob. [filtr czasowy](https://mapadanych.pl/dokumentacja/protokol.md#casovy-filtr).
- `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 najmniej `cache.ttl`, inaczej walidator zgłosi `live_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](https://mapadanych.pl/dokumentacja/protokol.md#vyvoj-v-case-a-trend).

### 5. Wizualizacja {#5-vizualizace}

| 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) {#6-endpoint-jen-u-mode-bbox-nebo-zivych-dat}

Zaimplementuj `GET` zgodnie z [kontraktem OpenAPI](https://mapadanych.pl/schema/0.1/provider-openapi.json):

- 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, ustaw `dm.truncated: true` i `dm.total`,
- własne parametry z `params` (zakres `od,do`, wiele wartości `a,b,c`),
- `Content-Type: application/geo+json` lub `application/json`, HTTPS, odpowiedź w ciągu 1 s,
- nieznane parametry query ignoruj (`lang`, `protocol`, `cursor` możesz pominąć).

CORS nie jest potrzebny – portal wysyła zapytania z serwera.

Python (FastAPI + PostGIS):

```python
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):

```ts
// 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 {#7-validace}

Powtarzaj, dopóki są błędy, i przejrzyj każde ostrzeżenie (napraw je albo świadomie pozostaw i uzasadnij):

```bash
# 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](https://mapadanych.pl/dokumentacja/api.md#kody-nalezu).

### 8. Przekazanie człowiekowi {#8-predani-cloveku}

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](https://mapadanych.pl/dokumentacja/dla-dostawcow.md#5-registrace).

## Zasady {#pravidla}

- **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 `properties` ani `dm`; portal wyświetla wszystko jako tekst.
- Nie umieszczaj tajnych kluczy w URL – manifest jest publiczny.
- Nie zmieniaj `provider.id` ani `dataset.id` po publikacji.
- Nie używaj kluczy `bbox`, `zoom`, `limit`, `cursor`, `lang`, `protocol` dla 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 {#kriteria-hotove-prace}

- [ ] Manifest jest dostępny przez HTTPS, a test na żywo walidatora zwraca `"valid": true`.
- [ ] Każdy zbiór ma `description`, `keywords`, `useCases`, `category` i (przy GeoJSON) `fields`.
- [ ] Licencja i atrybucja są potwierdzone przez człowieka.
- [ ] Współrzędne są w WGS84 `[lon, lat]`, brak ostrzeżeń `swapped_coordinates` i `outside_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` (brak `missing_trend_chart` i `trend_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) {#zadani-pro-agenta-ke-zkopirovani}

Człowiek może dać swojemu agentowi ten tekst:

```text
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.
```
