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 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&params=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&params=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.

Zsolt vagyok, full-stack fejlesztő.
Crawlereket, webalkalmazásokat, statikus honlapokat és interaktív vizualizációkat készítek.
Copyright © 2020 Zsolt Jurányi | All rights reserved.