GeoJSON összeállítása megyetérképhez
Adott a feladat, hogy egy weboldalon megjelenítsük kis hazánk megyetérképét, valamilyen érték szerinti színezéssel. Hogyan fogjunk neki?
Pár éve - jobb ötlet hiányában - én erre mindig rávágtam, hogy a Wikipédián fent van Magyarország megyetérképe SVG-ben, ahol az egyes megyéknek van id
attribútuma. Utóbbi alapján egyszerű őket CSS és JS segítségével formázni és interaktívvá tenni (pl. hover, click). Persze ezzel az SVG-s megoldással macerás megoldani a zoom-ot, a dragging-et, illetve ha később kitaláljuk, hogy városokat is rá kéne dobálni, valószínűleg meg vagyunk lőve.
Itt jön képbe a Leaflet.js library, amivel piszok egyszerűen lehet interaktív térképeket kreálni webes környezetben. SVG-t épít ő is, de a fenti funkciókat és még csillió más dolgot megvalósít nekünk. A drag mellett többféle zoom lehetőséget és könnyen hozzáadható vezérlőket, tooltip-eket, popup-okat biztosít, illetve több réteget tud kezelni egymásra pakolva.
Interaktív megyetérképhez van egy remek útmutatójuk is, jelen posztommal ezt egészíteném ki. 🙂
Ez a Choropleth tutorial USA adatok bemutatásáról szól, az államokat GeoJSON formában adagolja be a Leaflet-nek. A GeoJSON földrajzi adatokat ír le, de kiegészíthető egyéb adatokkal is, melyeket a térkép testreszabásakor felhasználhatunk. A cikkben nem írják le, hogy az amerikai államokat tartalmazó GeoJSON-t hogyan állították össze, így kicsit kutatnom kellett, hogyan tehetem meg ugyanezt Magyarország megyéivel.
Rákerestem Magyarország megyéire OpenStreetMap viszonylatban, és ezt a Boundaries oldalt találtam, ahol mind fel vannak sorolva egy-egy OSM relation ID kíséretében. Ezeket a relation-öket meg lehet tekinteni a térképen, amely mutatja a megye határvonalát.
Már csak az kell, hogy ezt a határvonalat megkapjuk GeoJSON formátumban. Felütöttem a guglit erre a kérdésre is, és erre a cikkre akadtam rá, mely megmondja az egyszerű megoldást. Az alábbi URL-ről letölthető az X azonosítójú relation GeoJSON fájlja:
http://polygons.openstreetmap.fr/get_geojson.py?id=X¶ms=0
Az ID-k és a hozzájuk tartozó GeoJSON-ök legyűjtése után persze utóbbiakat össze kell fűzni egy darab GeoJSON-be úgy, hogy a megyékhez kapott adatot becsomagoljuk Feature
objektumokba, majd az összeset egy FeatureCollection
-be.
Érdekesség, hogy a letöltött GeoJSON-ok nem teljesen validak, ugyanis GeometryCollection
-t írnak le, de csak egyetlen elemmel, amire a validátor visszapofázik. A megoldás az, hogy ki kell bújtani azt az egyetlen elemet a collection-ből.
A fentieket természetesen nem bonyolult automatizálni. Íme egy Node.js szkript, ami legyűjti Magyarország megyéit és egyetlen GeoJSON-ba fűzi őket:
// npm i cheerio got
const fs = require('fs');
const cheerio = require('cheerio');
const got = require('got');
async function getCountyOsmIds() {
const res = await got('https://wiki.openstreetmap.org/wiki/Hungary/Boundaries');
const $ = cheerio.load(res.body);
return $('h2:contains("Megyék") + table tr')
.toArray()
.map((tr) => {
return $('td a', tr)
.toArray()
.map((a) => $(a).text())[2];
})
.filter((id) => id);
}
async function downloadGeoJson(osmId) {
const geoJsonUrlTemplate = 'http://polygons.openstreetmap.fr/get_geojson.py?id=X¶ms=0';
const res = await got(geoJsonUrlTemplate.replace('X', osmId));
return res.body;
}
async function getGeoJson(id) {
const file = `${id}.json`;
if (fs.existsSync(file)) return fs.readFileSync(file);
const json = await downloadGeoJson(id);
fs.writeFileSync(file, json);
return json;
}
function fixGeometryCollection(geometry) {
if (geometry.type === 'GeometryCollection' && geometry.geometries.length === 1) {
return geometry.geometries[0];
}
return geometry;
}
function feature(geometry, id) {
return {
type: 'Feature',
properties: { id }, // will be useful :)
geometry: fixGeometryCollection(geometry),
};
}
async function getFeature(id) {
const geometryJson = await getGeoJson(id);
const geometry = JSON.parse(geometryJson);
return feature(geometry, id);
}
function featureCollection(features) {
return { type: 'FeatureCollection', features };
}
async function generateFeatureCollection(ids, file) {
const features = await Promise.all(ids.map(getFeature));
fs.writeFileSync(file, JSON.stringify(featureCollection(features)));
}
(async () => {
const ids = await getCountyOsmIds();
await generateFeatureCollection(ids, 'counties.json');
})();
A fenti szkripttel kapott GeoJSON egy-az-egyben hozzáadható a Leaflet térképhez, ahogy a fentebb linkelt Choropleth tutorial-ban le van írva. Ott a megyei adatokat is beleapplikálták a GeoJSON fájlba, én inkább csak a megyéhez tartozó OSM relation ID-t tettem bele, így bármilyen projekthez jó lesz az összeállított FeatureCollection
. Kliensoldalon nem lesz kunszt összekötni a feature.properties.id
-t az épp aktuális adatokkal és a megyék nevével.
Hasonló módszerrel rá tudjuk dobni a térképünkre Budapest kerületeit, vagy akár a szomszédos országokat is.