März 9th, 2012

Geoprocessing mit Open Source GIS Tools

Die Gewissheit

Im letzten Artikel habe ich einen Schnelldurchlauf durch die gängigsten FOSSGIS Werkzeuge vorgenommen um zu dem Schluss zu kommen, dass GRASS GIS bei rauhen Datenmengen einfach am besten abschneidet was die Performance angeht. Bei Verschneidungen mit kleinen Polygonen, wo sich PostGIS mit Hilfe des Räumlichen Index auf die relevanten Features konzentrieren kann ist die Leistung von PostGIS auch ganz gut. Mit dem Zerstückeln von großen Datensätzen durch die Verwendung von Vektorgittern kann man solche Verschneidungen auch um einiges beschleunigen. Allerdings hat man bei folgendem Fall wenig Chancen durch dieses Vorgehen Performance zu gewinnen: Wenn ST_Difference ins Spiel kommt.

Kurze Klärung: Nicht nur Verschneidungen sind in der GIS-Welt häufig, bei denen die Schnittmenge von zwei Ebenen berechnet wird – nein auch das Ausschneiden von Ausschlussgebieten kommt des öfteren vor. Dies entspricht dem Werkzeug “Erase” in der ArcGIS-Welt bzw. dem Tool “v.overlay operator=’not’” in GRASS GIS. In PostGIS kann man solche Schweinereien auch durchführen. Wer allerdings jetzt unbedacht zu ST_Difference greift, der sei gewarnt! Um das erwartete Ergebnis zu erhalten, sollte man die Differenz-Ebene zuerst mit ST_Union vereinigen, weil SQL eben Feature per Feature anfrägt und damit auf den ersten Blick “fehlerhafte” Ergebnisse liefert. Die Leistung ist jedoch in beiden Fällen eher dünn.

Das nachfolgende Zitat aus einem Beitrag in der PostGIS Mailing-Liste erklärt warum:

By it’s nature, using SQL for spatial computation is most efficient for
operations which can be carried out in a feature-wise manner.
Unfortunately, overlay does not fall into this category (since there is
a large amount of interaction between features.
Implementing a more efficient overlay algorithm in PostGIS is a nice challenge for the future… [1]

Soll heissen: Große Datenmengen und “Erase” gehen bei PostGIS nur mit seeeehr viel Geduld. Ich bin spätestens an dieser Stelle zu GRASS GIS gewechselt. Nicht nur ArcGIS nützt nämlich effiziente Algorithmen zum Verschneiden von kompletten Ebenen. Übrigens: seit Mitte Februar gibts jetzt GRASS GIS in der stabilen Version 6.4.2. Was? Und 64Bit? Aber sicher.. Wann kommt nochmal ArcGIS 64Bit?

Be the first to like.
Januar 20th, 2012

Geoprocessing mit Open Source GIS Tools und Python

Aufgabenstellung

Momentan soll ich eine ganze Menge an Geodaten für einen wissenschaftlichen Auftraggeber verarbeiten. Dabei geht es um die Potentialabschätzung der Wuchsleistung von schnellwachsenden Baumarten. Ich freute mich riesig auf den professionellen Einsatz von Open Source GIS Tools unter etwas anspruchsvolleren Bedingungen – denn  in letzter Zeit hatte ich beruflich wenig mit dem OS GIS Universum zu tun.

Große Datensätze

Anspruchsvoll? Oh ja – es handelt sich um eine bayernweite Analyse und da kommt schnell einiges zusammen…
Hier eine kleine Übersicht:

  • 4 Sachdatenbanken (MS Access)
  • 9 vektorielle Geodatensätze in insgesamt über 750 einzelnen ESRI Shapefiles (ca. 6,8 GB)
  • 3 Rasterdatensätze als ESRI ASCII bzw. Binary GRID (ca. 4,6 GB)

Nicht übel. Diese Menge von rund 11 GB sollte also mit Open Source GIS Tools verarbeitet werden. Das war im Auftrag natürlich nicht gefordert, da dieser softwareunabhängig formuliert war – aber für mich war es klar, dass ich FOSSGIS-Tools einsetze.

Ehrlich gesagt habe ich bislang mit FOSSGIS-Werkzeugen keine aufwendigen GIS-Analysen durchgeführt, sondern eher ausprobiert, was damit alles geht. Nun also eine vollständige Analyse. Dass das spannend wird, dachte ich mir schon…

Welche Werkzeuge?

Nun gut – welches Werkzeug solls denn nun sein? Da die Daten – vor allem die Shapefiles – ziemlich zerhackstückelt über ganz Bayern daher kamen (Schönen Gruß ans LfU und das LVG), mussten diese erstmal zu bayernweiten Datensätzen zusammengeführt werden. Das war vor Angebotsabgabe leider nicht bekannt – aber gut, ich nahm die Herausforderung an.

QGIS – fTools

Bei QGIS gibts eine Erweiterung, welche auch Shapefiles zusammenführen kann. Die sog. fTools können dies und auch noch viel mehr (z.B. Intersection, Dissolve, Multipart to single part etc.). Dabei basiert fTools auf QGIS und GDAL/OGR, um die Geoprocessing-Funktionen anbieten zu können. Außerdem ist es in Python geschrieben.
Die Variante der fTools in QGIS kam aufgrund der doch langen Laufzeiten nicht in Frage. Die GUI von QGIS frisst da anscheinend doch einiges an Performance — außerdem muss fTools dann als Plugin von QGIS mit dem Mutterprozess hin und her kommunizieren. Das wollt ich mir ersparen.
Man muss aber sagen, dass die fTools für kleinere Batch-Arbeiten gut geignet sind. Das Pythonskript, das ich mit der Vorlage der fTools geschrieben habe liest sich einfach. Das QGIS-Objektmodell folgt dem üblichen “Feature – Geometry – Attributtable”-Schema und ist somit leicht nachzuvollziehen.
Bis die QGIS-Pythonbindings dann aber mal ansprechbar sind kann ne Zeit vergehen. Gary Sherman hat dazu eine ausführliche Anleitung hier. Dazu mal an anderer Stelle mehr.

GDAL/OGR – kennste eine – kennste alle?

GDAL/OGR ist ja das ETL (Extract-Transform-Load) Werkzeug schlechthin. Sagt man. Ist auch so sag ich jetzt, aber noch vor ein paar Wochen hatte ich noch nicht viel Ahnung, wie man die Bibliothek einsetzt.
Ich hatte GDAL/OGR schon immer unbedacht benutzt, wenn ich z.B. mit QGIS oder GRASS arbeitete, da viele Desktop-GIS diese Bibliothek unter der Haube haben – allerdings eben immer “nur” als Datenprovider für Desktop GIS.

Nun war klar, dass ich die Python-Bindings von GDAL/OGR genauer anschauen muss. Also erstmal recherchieren und viel ausprobieren (die Python-Doku für GDAL/OGR ist derzeit noch sehr spärlich) und irgendwann klappts dann aber auch. Das grundsätzliche Modell bei Geoprocessing-Bibliotheken ist ja immer relativ ähnlich: es gibt ein Feature-Objekt mit Sachdaten und ner Geometrie und man hat ein paar GIS-Methoden (welche im FOSSGIS-Universum ja eigentlich fast immer auf GEOS und somit der JTS basieren) zur Verfügung (Intersection, Buffer, etc.). Wie soll es auch mordsmäßig verschieden sein?
Genau so ists auch bei den Python-Bindings von GDAL/OGR. Die Extract und Load-Komponente kommt von GDAL/OGR selbst. Die geometrischen Verschneidungsoperatoren werden bei GDAL/OGR mit GEOS bewerkstelligt, während Transformationen mit PROJ4 behandelt werden.

Wer – wie ich – eher aus der ESRI-Ecke mit dem ArcGIS Geoprocessor kommt, wird sich bei GDAL/OGR in Python auch recht schnell zurecht finden. Die FDO API in C# war da schon ne andere (weil abstraktere) Nuss.

Interessante Links für GDAL/OGR mit Python:

Letztlich konnten die Daten dann mit einem  Pythonskript und der GDAL/OGR-Bibliothek zu bayernweiten Datensätzen zusammengeführt werden. Und wie schnell das geht! Da ist GDAL/OGR wirklich in ihrem Element. Wahnsinn, was die Bibliothek wegschreibt:

  • rund 100 Shapefiles mit insgesamt 2,1 Mio Features in 20 Minuten (800MB)
  • rund 100 Shapefiles mit insgesamt 3,1 Mio Features in 35 Minuten (4,2 GB)

Diese Zahlen überzeugen.

PostGIS

Auch PostGIS fällt nicht aus der Reihe der FOSSGIS-Tools und basiert auf GEOS und der PROJ4-Bibliothek. Die Kombination von SQL als Datenabfragesprache mit geometrischen Funktionen (Spatial SQL) fand ich immer spannend. Nachdem die Shapefiles also zusammengeführt waren, wollte ich einfach alle Vektordaten in eine PostGIS-DB importieren und die Aufbereitung in PostGIS machen. Auch dass mit PL/Python meine Lieblingssprache Python in einer Datenbank als prozedurale Sprache zur Verfügung steht find ich klasse (ich denke hier an die Lesbarkeit von PL/SQL Code vs. Python…).
Allerdings kam ich nicht weit. PostGIS hat definitiv als professionelle Geodatenhaltung seine Stärken und einige Anfragen an Daten kann es auch schnell beantworten, aber PostGIS ist nicht “post GIS” (im Sinne von “nach GIS”) wie es im Vorwort des Buchs “PostGIS in Action” hies. Man braucht bei solchen Datenmengen anscheinend schon noch ein extra Analysewerkzeug. Vielleicht liegts auch an meiner Rechenpower oder meinen PostGIS/SQL-Kenntnissen, aber GIS-Analysen mit dieser Datenmenge klappt nach meiner Erfahrung nicht in PostGIS.  Nach Laufzeiten von 2 Tagen habe ich die aufwendigen Verschneidungen (bayernweit) sein gelassen. Schade eigentlich. Ich hätte irgendwie mehr Performance von PostGIS erwartet.

SAGA GIS

Raster? Nimm SAGA GIS. Eben – für Rasterdaten ist SAGA GIS spitze. Performant, gut bedienbar, sogar per Python oder Kommandozeile ansprechbar für Batchabläufe. Bei Vektordaten hat SAGA aber so seine Macken. Außerdem werden nicht viele Möglichkeiten in der Vektoranalyse angeboten. Diejenigen, die dabei sind basieren interessanterweise jedoch nicht auf GEOS, sondern auf CGAL.
Aber gut, SAGA GIS ist eben vor allem ein Raster-GIS. In Ordnung.

GRASS GIS – hat so nen Bart?

Allerdings. GRASS GIS hat schon einige Jahre auf dem Buckel (30+?) – wird aber laufend fortentwickelt. Seit geraumer Zeit sieht auch die GUI etwas ansprechender aus. Von den Analysemöglichkeiten her ist GRASS GIS im FOSSGIS-Universum meiner Meinung nach einsame spitze.

Nimm GRASS GIS – wenn Du nicht ewig Zeit hast. Im Zusammenhang mit Scripting ist das Werkzeug ein verdammt performantes Analyse-Tool. Natürlich sollte man an dieser Stelle die etwas höhere Lernkurve bei GRASS ggü. anderen Werkzeugen berücksichtigen. Aber die Zeitinvestition lohnt sich!

Ich bin in den o.g. Ausführungen immer auf die Geometriebibliothek der Tools eingegangen. Da fast alle OS GIS Werkzeuge letztlich die geometrischen Funktionen an die GEOS-Bibliothek weiterreichen, ist auch die Performance bei großen Datenmengen vergleichbar.
Damit ist es relativ egal, ob man GDAL/OGR oder die fTools von QGIS oder PostGIS verwendet. Alle basieren auf GEOS. GEOS ist ja ein C++ Port der JTS (Java Topology Suite). Und diese ist wiederum in anderen FOSSGIS-Tools verbaut wie gvSIG, OpenJUMP, uDIG oder Kosmo. GIS-Analysen mit derartigen Datenmengen in Java-Tools durchzuführen hatte bei mir leider regelmäßig einen “Out of heap space” Fehler zur Folge. Deshalb schied auch die JTS aus.

Die GRASS GIS Overlay-Operatoren sind jedoch als C-Algorithmen anscheinend richtig speicheroptimiert geschrieben. Trotzdem bleibt ein kleiner Wermutstropfen: Auch in GRASS GIS konnte ich die Datenmengen nicht bayernweit importieren bzw. verschneiden. Mit einem 64bit System und mehr RAM wäre das vielleicht schon gegangen. So aber blieb mir nur ein “Tiled Processing”, in dem ich die bayernweiten Datensätze in 16 Kacheln aufgeteilt habe. Mit GRASS GIS hats dann im Batch-Modus per Pythonbindings allerdings recht flott (rund 1,5 Std. für einen bayernweiten Verschneidungsdurchlauf) geklappt. Vielleicht mach ich demnächst auch nochmal nen Vergleich mit einem Tiled Processing der Daten in PostGIS, aber ich habe wenig Hoffnung, dass dies ähnlich performant ist wie GRASS GIS.

Und ne kleine GRASS Python Utility Klasse ist auch noch herausgekommen:

# -*- coding: latin-1 -*-
#  Helper for GRASS GIS Python Scripting
#  Johannes Sommer, 18.01.2012

class utility():
	import core as grass
	''' Utility-Class for GRASS GIS Python Scripting.
		Copyright by Johannes Sommer 2012.

		- doAddColumn
		- doCalcColumnValue
		- doRenameColumn
		- doIntersection
		- doDifference
		- getMapsets
		- getVectorLayers
		- getRasterLayers
		- getVectorLayersFromMapset
		- getRasterLayersFromMapset
		- getColumnList
		- doFilterLayerList 

		Usage:
		import grass.utils as utils
		gUtil = util.utility()
		gUtil.dodifference('forest', lakes')
		'''

	def doAddColumn(self, _lyrA, _colName, _colType):
		''' Adds a column to the input vector layer with the given type.

			GRASS datatypes depend on underlying database, but all
			support:
			- varchar (length)
			- double precision
			- integer
			- date

			Usage:
			doAddColumn('lakes', 'width', 'double precision')	'''
		retVal = grass.run_command('v.db.addcol', map=_lyrA,
								   columns='%s %s' % (_colName, _colType) )
		return retVal

	def doCalcColumnValue(self, _lyrA, _colName, _newVal):
		''' Updates a column of the input vector layer with the passed value.
			@_newVal can be a certain value or a calculation of existing columns.

			Usage:
			doCalcColumnValue('lakes', 'width', 'length/2')		'''
		retVal = grass.run_command('v.db.update', map=_lyrA,
								   column=_colName, value=_newVal)
		return retVal

	def doRenameColumn(self, _lyrA, _colNameA, _colNameB):
		''' Renames a column of the input vector layer.

			Usage:
			doRenameColumn('lakes', 'width', 'width_old')		'''
		retVal = grass.run_command('v.db.renamecol', map=_lyrA,
								   column='%s,%s' %(_colNameA,_colNameB))
		return retVal

	def doIntersection(self, _lyrA, _lyrB, OVERWRITE_OUTPUT==False):
		''' Performs a geometric intersection ('AND'-Operator) of two vector layers
			and writes the output to a layer called 'layerNameA_X_layerNameB'.

			@OVERWRITE_OUTPUT can be set to 'Y' or 'N'
			Usage:
			doIntersection('lakes', 'forest')	'''
		outputLyr = _lyrA.split('@')[0] + '_X_' + _lyrB.split('@')[0]

		optArg = ''
		if OVERWRITE_OUTPUT == True:
			optArg = '--overwrite'
		retVal = grass.run_command('v.overlay %s' % optArg, ainput=_lyrA,
								   binput=_lyrB, output=outputLyr,
								   operator='and')
		return retVal

	def doDifference(self, _lyrA, _lyrB, OVERWRITE_OUTPUT==False):
		''' Performs a geometric difference ('NOT'-Operator) of two vector layers
			and writes the output to a layer called 'layerNameA_DIFF_layerNameB'.

			@OVERWRITE_OUTPUT can be set to 'Y' or 'N'
			Usage:
			doDifference('lakes', 'forest')	'''
		outputLyr = _lyrA.split('@')[0] + '_DIFF_' + _lyrB.split('@')[0]

		optArg = ''
		if OVERWRITE_OUTPUT == True':
			optArg = '--overwrite'
		retVal = grass.run_command('v.overlay %s' % optArg, ainput=_lyrA,
								   binput=_lyrB, output=outputLyr,
								   operator='not')
		return retVal

	def getMapsets(self):
		''' Returns all GRASS mapsets in current GRASS location as list.'''
		return grass.mapsets()

	def getVectorLayers(self):
		''' Returns all vector layers in current GRASS location as list.'''
		return grass.list_strings('vect')

	def getRasterLayers(self):
		''' Returns all raster layers in current GRASS location as list.'''
		return grass.list_strings('rast')

	def getVectorLayersFromMapset(self, _mapset):
		''' Returns all vector layers in given mapset as list.'''
		vLyrList = getVectorLayers()
		# Filter List by MAPSET
		vLyrListFiltered = []
		for item in vLyrList:
			if _mapset in item:
				vLyrListFiltered.append(item)
		return vLyrListFiltered

	def getRasterLayersFromMapset(self, _mapset):
		''' Returns all raster layers in given mapset as list.'''
		rLyrList = getRasterLayers()
		# Filter List by MAPSET
		rLyrListFiltered = []
		for item in rLyrList:
			if _mapset in item:
				rLyrListFiltered.append(item)
		return rLyrListFiltered

	def getColumnList(self, _vLyr):
		''' Returns all column names of a vector layer as list.'''
		# Import vector wrapper from %GISBASE%\etc\python\script
		import vector as vector
		out_colList = []
		out_colList = vector.vector_columns(_vLyr, getDict = False)
		return out_colList

	def doFilterLayerList(self, _lyrList, _lyrkey):
		''' Filters input layer list per _lyrkey and returns filtered list.'''
		out_lyrList = []
		for item in _lyrList:
			if _lyrkey in item:
				out_lyrList.append(item)
		return out_lyrList

3 Leute mögen diesen Artikel.
Dezember 22nd, 2011

UNIGIS MSc – ein Resumee

Ungefähr vor einer Woche war es soweit: Ich erhielt das Abschlusszeugnis vom UNIGIS-Team und darf mich von jetzt an offiziell einen “Master of Science” (GIS) nennen. Nach drei Jahren Fernstudium tut es gut, das angestrebte Ziel endlich abgeschlossen zu wissen.
Zeit für ein Resumee in Sachen UNIGIS!

1. Kosten
Zunächst ist es einmal so, dass dieses Fernstudium eine Menge Geld kostet. Rund 8000 Euro werden da für die Weiterbildung verbraten. Wer einen Arbeitgeber hat, der ihm was zuschießt kann sich glücklich schätzen. Wer das privat finanziert sollte bei der steuerlichen Absetzbarkeit der Kosten aufpassen. Bei UNIGIS kann man die Kosten in zwei Raten oder in einer Rate zahlen. Ich dachte, ich nehm die eine Rate – dadurch spart man sich ein paar hundert Euro ggü. UNIGIS. Allerdings kann man dann bei der Steuererklärung den Betrag als Werbungskosten auch nur in einem Jahr absetzen. Da dieser Betrag mit ca. 8000 Euro den überhaupt möglichen absetzbaren Maximalbetrag von 4000 Euro übersteigt, “verpuffen” 4000 Euro und wirken nicht steuermindernd. Deshalb würde ich aus heutiger Sicht die Zweimal-Rate in Anspruch nehmen, um die tatsächlichen Kosten steuerwirksam zu halten.
Aber: ich bin kein Steuerexperte und übernehme keinerlei Gewähr für diesen Hinweis.

2. Inhalte
Die Inhalte der Module waren insgesamt sehr spannend und haben einen vollständigen Einblick in das Thema GIS vermittelt. Je nach Vorkenntnissen ist man in einigen Modulen schneller oder etwas langsamer unterwegs, aber die 6 Wochen Bearbeitungszeit hatten bis auf ein Modul immer ausgereicht. Super waren die Module zur Datenmodellierung, Geodatenbanksysteme und zur Räumlichen Analyse. Was mir im Angebot der optionalen Module ein bisschen gefehlt hat  war GIS-Programmierung in .NET. Mittlerweilen gibts allerdings ein Modul dazu mit ArcObjects und VB.NET. Trotzdem würde ich mir zusätzliche Angebote dazu wünschen. Auch mehr Open Source Module könnten das Angebot der optionalen Module noch verbessern (z.B. PostGIS, DotSpatial oder SharpMap, QGIS und Anpassungen mit Python). Hier ist man sehr proprietär unterwegs.

3. Betreuung
Die Betreuung ließ in manchen Modulen zu wünschen übrig. Da hatte ich bei zwei Modulen schon das Gefühl, dass jemand nen ziemlichen lockeren Job als Lehrbeauftragter hat. Mit einer Beurteilung war ich auch überhaupt nicht einverstanden, da mir die Leistungserwartung des Modulbetreuers (Modul 3) nicht ausreichend genau formuliert war. Da gingen unsere Vorstellung von der Bearbeitung des Moduls etwas auseinander. Auch nach heftiger Diskussion – letztlich mit der Lehrgangsleitung – hat sich nichts an der Beurteilung geändert. Insgesamt war die Betreuung in den Pflichtmodulen jedoch gut.
Während der gesamten Studienzeit war die Betreuung durch eine Tutorin eine wichtige Unterstützung. Unsere Jahrgangstutorin des MSc 2009 war dabei sehr aktiv und motivierend. Vielen Dank an Julia Moser dafür!

4. Master Thesis
Die Phase “Master Thesis” dauerte bei mir (auch durch einen Arbeitgeberwechsel in der Fernstudiumszeit) insgesamt etwas länger. Nachdem ich die Module alle hinter mich gebracht hatte, gingen noch 1,5 Jahre ins Land bis die Masterarbeit fertig war. Die eigentliche Bearbeitung lag bei einem Jahr. Ursprünglich hatte ich 6 Monate im Kopf, aber das ging einfach nicht.
Ich hatte ein betriebliches Thema gewählt und hatte auch mit meinem Arbeitgeber vereinbaren können, Arbeitszeit für die Master Thesis zu verwenden, da es ja auch einen betrieblichen Nutzen hatte. Dieses Vorgehen hat Vorteile:

  • Man lernt das Unternehmen und Prozesse besser kennen
  • Man macht das meiste in der Arbeitszeit und nicht noch abends oder am Wochenende
  • Man bearbeitet etwas, was im Idealfall auch umgesetzt wird

Allerdings ist man dann auch abhängig von der Zuarbeit und Informationen von Mitarbeitern, muss auf Termine warten, muss nebenher auch noch das reguläre Tagesgeschäft erledigen und oft auch andere Arbeiten höher priorisieren.
Zum Schluss hatte ich mit meinem Vorgesetzten vereinbart, 14 Tage nur die Master Thesis zu bearbeiten, weil es sich sonst noch länger hingezogen hätte.
Insgesamt haben mich mein Arbeitgeber und meine Kollegen sehr gut bei der Master Thesis unterstützt. Außerdem möchte ich die extrem kurzen Antwortzeiten per email und die gute Betreuung durch Prof. Strobl herausheben – das war wirklich klasse!

Fazit
Wer ein Interesse an GIS mitbringt und dieses auch über längere Zeit aufrecht erhalten kann schafft das Fernstudium auch. Allerdings gibts natürlich auch Durststrecken in Modulen, die einem nicht so liegen. Bei mir war dies das Modul zum Thema Projektmanagement. Dieses Fernstudium bedeutet definitiv eine Herausforderung mit weniger Zeit für Hobbies, Freunde und Familie. Aber wenn einen das Thema GIS wirklich juckt und wer gleichzeitig im Beruf noch nebenher Geld verdienen möchte (muss), dann gibts meiner Meinung nach derzeit keine Alternative zu UNIGIS.

3 Leute mögen diesen Artikel.
November 20th, 2011

UNIGIS MSc: Master Thesis abgeschlossen!

Nach einem Jahr Bearbeitungszeit war es am Freitag nun soweit: ich habe meine Master Thesis abgegeben. Hurra!

Gefordert war neben der Master Thesis in zweifacher, gedruckter und gebundener Ausfertigung auch ein Extended Abstract (8 Seiten) und eine Präsentation (ca. 20 Seiten) abzuliefern. Die Thesis selbst, der Extended Abstract und die Präsentation mussten dann auch digital nach Salzburg geschickt werden.

Der Titel der Master Thesis ist:

Konzeption eines Wegeinformationssystems und Aspekte der
Umsetzung

am Beispiel der Bayerischen Staatsforsten

Die Master Thesis kann hier bezogen werden und – wer es etwas knapper bevorzugt – einen Auszug der Arbeit in Form des Extended Abstract findet man dort auch. Dem eiligen Leser würde ich nach dem Genuss des Abstracts aber auf jeden Fall Kap. 5, 6 und 7 der gesamten Arbeit (insgesamt 10 Seiten) ans Herz legen.

Es war vor allem in den letzten 2 Wochen eine heftige Zeit, denn bekanntlich kommt in diesen heiklen Momenten dann vieles zusammen (Kind und Partnerin krank, wichtige Termine in der Arbeit). Dennoch hab ich es geschafft, meinen im Oktober gesetzten Abgabetermin einzuhalten. Das war auch nötig, da sich der Zeitplan durch mehrere Ereignisse immer wieder erweitert hatte.

Ursprünglich hatte ich vor, die Thesis in 6 Monaten abzuhandeln – was auch von UNIGIS so vorgegeben wird. Dieser Zeitrahmen hatte sich dann aber bald als nicht vereinbar mit einem regulären Vollzeit-Job und Familie herausgestellt. Sei es wie es ist – mein Sohn würde sagen:

“Fertig bin!”

 

1 andere Person mag diesen Artikel.
September 13th, 2011

Gekachelte Erstellung von Höhenlinien aus einem DGM10

Ausgangslage

Aus einem Digitalen Geländemodell (DGM) lassen sich relativ einfach Höhenlinien berechnen. In ArcGIS gibt es dazu das Tool Contour, was als Input ein DGM und einige wenige Parameter benötigt. Für kleinere Datenmengen – also entweder ein DGM, das ein großes Gebiet abdeckt und dabei eine grobe Auflösung besitzt, oder aber ein DGM eines kleinen Gebietsausschnitts mit hoher Auflösung funktioniert das Werkzeug auch einwandfrei und die Ableitung zackig. Die Berechnung der Höhenlinien im Abstand von 25m mit dem DGM25 (50m Auflösung, ca. 200MB) für ganz Bayern benötigt ungefähr 40 Sekunden. Dabei ist die entstehende Shapedatei 300MB groß.

Mit einem DGM10 (10m Auflösung) über ganz Bayern hinweg siehts dann aber schon anders aus. ArcGIS 9.3.1 sp2 machte bei diesen Datenmengen (DGM: 2,0 GB als Rasterdataset in File Geodatabase) schlapp. Denn der temporäre Output des Werkzeugs Contour ist ausschließlich im Shapefile-Format. Und dieses Format hat leider die etwas unangenehme Eigenschaft, höchstens 2,1 GB an Daten aufnehmen zu können (siehe hier). Wenn man mit dieser Auflösung des DGM10 über ganz Bayern hinweg Höhenlinien ableiten möchte, werden aber einiges mehr als 2,0 GB erzeugt. Was also tun?

Die Lösung heisst hier: Gekachelte Verarbeitung (Tiled Processing). Man teilt also das DGM in ein paar Kacheln auf (Create Fishnet des DGM-Extents, Polygonisieren der Linien, Clippen des DGM mit dem Polygon-Raster) und leitet aus diesen die Höhenlinien ab. Gedacht getan – aber halt: zu kurz gedacht! Denn die Ableitung der Höhenlinien basiert auf einer Interpolation einer Linie zwischen den Höhenpunkten. Was passiert also an den Rändern der Kacheln? Genau: es entstehen Lücken. Und zwar genau in dem Ausmaß der Auflösung des DGMs. In unserem Fall entspricht dies also einem 10m-Streifen.

Tja nun – und jetzt? Keine Panik. Denn auch dafür gibts eine Lösung.

 

 

Lösung

Der Trick bei der gekachelten Ableitung der Höhenlinien besteht darin, gepufferte Kacheln des DGM zu prozessieren. Dabei muss der Puffer um die Kacheln mindestens das zweifache der Auflösung des DGMs betragen. Es werden also aus überlappenden Kacheln des DGM die Höhenlinien pro Kachel berechnet. Anschließend liegen die Höhenlinien aus den Überlappungsbereichen exakt übereinander.

An dieser Stelle räumt ein Clip auf den Höhenlinien mit den ursprünglichen Extents der Kacheln wieder auf. Diese tolle Idee stammt aus einem ESRI-Forum und ist hier nachzulesen.

Und tatsächlich, beim Test von drei Kacheln lagen die berechneten Höhenlinien aus den gepufferten Kacheln im Überlappungsbereich exakt übereinander. Jetzt muss nur noch ein Clip mit den Extents der ungepufferten Kacheln die beiden Höhenlinien-Shapes trennen und siehe da: die Höhenlinien passen an den Rändern der Kacheln wunderbar zueinander.


Be the first to like.
Tags: , ,
Mai 30th, 2011

ArcSDE lesen mit OpenSource GIS in .NET

In diesem Artikel möchte ich zeigen, wie man Geometrieobjekte aus einer ESRI ArcSDE-Datenbank mit einem OpenSource GIS Client visualisieren lassen kann. Dieser Ansatz ist nicht komplett quelloffen, da sich die verwendeten Feature Data Object (FDO) API auch der proprietären ArcSDE API bedienen. Als GIS-Viewer wird die Bibliothek DotSpatial eingesetzt. In einer vor kurzem veröffentlichten Artikelserie habe ich beschrieben, wie man die FDO API mit DotSpatial in C# als Datenprovider verwenden kann. Als beispielhafte Datenquelle diente ein ESRI Shapefile.

Bei einer ArcSDE als Datenquelle wirds nur leicht anders. Das ist das Schöne an der FDO API: wenn man das Konzept und die Vorgehensweise einmal begriffen hat, ist die Verwendung der FDO API unabhängig vom Datenprovider. Nur einige wenige Dinge ändern sich.

Voraussetzungen
Die FDO API ist bezüglich des Datenproviders für eine ArcSDE abhängig von der proprietären ESRI ArcSDE API. Die entsprechenden DLLs von ESRI müssen deshalb im Systempfad zu finden sein. Eine detailliertere Beschreibung findet sich auf den Seiten der FDO API. Ein kurzer Auszug der Informationen, die sich zwar auf ArcGIS 9.1 beziehen, jedoch im wesentlichen auch für 9.3.1 gelten.

The operation of FDO Provider for ArcSDE is dependent on the presence of ArcSDE 9 and a supported data source, such as Oracle 9i, in the network environment. The host machine running FDO Provider for ArcSDE must also have the required DLLs present, which are available by installing either an ArcGIS 9.1 Desktop application or the ArcSDE SDK. For example, the required DLLs are present if either ArcView®, ArcEditor®, or ArcInfo® are installed. For more information about ArcGIS 9.1 Desktop applications and the ArcSDE SDK, refer to the ESRI documentation.

Specifically, in order for FDO Provider for ArcSDE to run, three dynamically linked libraries, sde91.dll, sg91.dll, and pe91.dll, are required and you must ensure that the PATH environment variable references the local folder containing these DLLs. For example, in Microsoft Windows, if ArcGIS 9.1 Desktop is installed to C:\Program Files\ArcGIS, then the required ArcSDE binaries are located at C:\Program Files\ArcGIS\ArcSDE\bin. Similarly, if the ArcSDE SDK is installed to the default location, then the required ArcSDE binaries are located at C:\ArcGis\ArcSDE\bin. The absence of this configuration may cause the following exception message “The ArcSDE runtime was not found.”.

Einzige Veränderung gegenüber der Version von 9.1 ist mittlerweile, dass die benötigten DLLs wohl nun im Verzeichnis “C:\Program Files\ArcGIS\Bin” liegen. Das Verzeichnis ArcSDE gibt es auf der Clientseite (ArcGIS Desktop-Installation) nicht mehr.

Datenprovider-String

Der Datenprovider für die ArcSDE muss wie bei jedem anderen FDO-Provider auch dem ConnectionManager als Argument mitgeteilt werden.

FDORegistry = FeatureAccessManager.GetProviderRegistry();
FDOManager = FeatureAccessManager.GetConnectionManager();
FDOConnection = FeatureAccessManager.GetConnectionManager().CreateConnection("OSGeo.ArcSDE.3.6");

Verbindungsparameter

Die Verbindungsparameter sind im Gegensatz zum ESRI Shapefile etwas umfangreicher. Es müssen die im Datenbankbereich üblichen Verbindungseigenschaften folgendermaßen gesetzt werden:

IConnectionPropertyDictionary connectionPropertyDictionary;
connectionPropertyDictionary = FDOConnection.ConnectionInfo.ConnectionProperties;
connectionPropertyDictionary.SetProperty("Server","localhost");
connectionPropertyDictionary.SetProperty("Instance","5151");
connectionPropertyDictionary.SetProperty("Username","gisuser");
connectionPropertyDictionary.SetProperty("Password","password");
connectionPropertyDictionary.SetProperty("Datastore","Default Datastore");

Wobei die sog. Datastore zunächst einmal auf “Default Datastore” gesetzt werden kann. Auf der Seite der FDO API findet man folgende Info dazu:

An ArcSDE data source may contain more than one data store. For the first call to Open(), a data store name is optional. If successful, the first call to Open() results in the data store parameter becoming a required parameter and a list of the names of the data stores in the data source becoming available. You must choose a data store and call Open() again.

Das Auswählen der Datastore funktioniert in C#-Code übersetzt folgendermaßen:

string value;
bool isRequired;
bool isEnumerable;
string[] strArry = null;
IConnectionPropertyDictionary dict = FDOConnection.ConnectionInfo.ConnectionProperties;
foreach (string st in dict.PropertyNames) {
	value = dict.GetProperty(st);
	isRequired = dict.IsPropertyRequired(st);
	isEnumerable = dict.IsPropertyEnumerable(st);

	if (isEnumerable && st == "Datastore")
	{
		strArry = new string[dict.EnumeratePropertyValues(st).Length];
		dict.EnumeratePropertyValues(st).CopyTo(strArry,0);
	}
}

Damit haben wir schon alles, was wir für den ArcSDE-Zugriff mit der FDO API in .NET brauchen. Das Verfahren, die Attributdaten und die Geometriedaten per FeatureReader zu lesen und in ein DotSpatial FeatureSet zu übersetzen ist identisch mit dem Verfahren beim ESRI Shapefile in den Artikeln 1,2 und 3.

Eine Besonderheit noch gegenüber dem Auslesen von Shapefiles ist natürlich, das eine ArcSDE mehrere Schemata besitzen kann, in denen widerum mehrere Feature Classes vorhanden sind. Mit der FDO API geht man jedoch nicht den hierarchischen Weg von Datenbank -> Schema -> Feature Class.

Vielmehr ist es hier so, dass die Feature Classes einen voll qualifizierenden Namen bekommen. Das heisst, die Feature Classes werden mit der FDO API nach folgendem Muster zugänglich gemacht, wenn man sich erfolgreich zur Datenbank verbunden hat:

<SCHEMA>:<FEATURE CLASS>

Ein Beispiel:

GISDYN:WEGE” steht für die Feature Class “WEGE”, die im Schema “GISDYN” angesiedelt ist. Beim setzen des Feature Class Namens sollte dies also berücksichtigt werden.

Mit dieser Info versteht man auch den unten beigefügten Code der Methode ListTablesOfDB() besser.

Versionierten SDE-Zugriff habe ich jedoch nicht getestet. Der standardmäßige Zugriff erfolgt wohl über die Version SDE.DEFAULT.

Als Anhang noch hier kompletten Code zum Zugriff auf eine ArcSDE über die FDO API:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using OSGeo.FDO;
using OSGeo.FDO.Connections;
using OSGeo.FDO.ClientServices;
using OSGeo.FDO.Geometry;
using OSGeo.FDO.Commands;
using OSGeo.FDO.Commands.Feature;
using OSGeo.FDO.Expression;
using OSGeo.FDO.Commands.DataStore;
using OSGeo.FDO.Commands.Schema;
using OSGeo.FDO.Filter;
using OSGeo.FDO.Schema;
using DotSpatial.Data;

namespace GSharpDotSpatial
{

	public class ArcSDEHelper
    {
		private string hostname;
		private int port;
		private string user;
		private string password;
		private string datastore;
		private string connStr;
		private string pkeyColumn;
		private DataTable dataTable;

		private IConnection FDOConnection;
        private IProviderRegistry FDORegistry;
        private IConnectionManager FDOManager;
        private OSGeo.FDO.Connections.ConnectionState ConState;

        public ArcSDEHelper(string _hostname, int _port, string _user, string _password, string _datastore)
        {
            this.hostname = _hostname;
			this.port = _port;
			this.user = _user;
			this.password = _password;
			this.datastore = _datastore;

			connStr = "Server=" + hostname + ";" + "Instance=" + port + ";" + "User=" + user + ";" + "Password=" + password + ";" + "Datastore=" + datastore + ";";
        }

        public IConnection connect()
		{
        	try
        	{
			    FDORegistry = FeatureAccessManager.GetProviderRegistry();
				FDOManager = FeatureAccessManager.GetConnectionManager();
				FDOConnection = FeatureAccessManager.GetConnectionManager().CreateConnection("OSGeo.ArcSDE.3.6"); // Replace the version of DLL your using…

				IConnectionPropertyDictionary connectionPropertyDictionary;
				connectionPropertyDictionary = FDOConnection.ConnectionInfo.ConnectionProperties;
				connectionPropertyDictionary.SetProperty("Server",hostname);
				connectionPropertyDictionary.SetProperty("Instance",port.ToString());
				connectionPropertyDictionary.SetProperty("Username",user);
				connectionPropertyDictionary.SetProperty("Password",password);
				connectionPropertyDictionary.SetProperty("Datastore",datastore);

//				FDO Gets the Datastore options out of a pending connection (2-step connection)
//				FDOConnection.Open();
//				string s = "";
//				string[] strArry = null;
//				IConnectionPropertyDictionary dict = FDOConnection.ConnectionInfo.ConnectionProperties;
//				foreach (string st in dict.PropertyNames) {
//
//					string val = dict.GetProperty(st);
//					string defVal = dict.GetPropertyDefault(st);
//					string localname = dict.GetLocalizedName(st);
//					bool isRequired = dict.IsPropertyRequired(st);
//					bool isEnumerable = dict.IsPropertyEnumerable(st);
//
//					if (isEnumerable)
//					{
//						strArry = new string[dict.EnumeratePropertyValues(st).Length];
//						dict.EnumeratePropertyValues(st).CopyTo(strArry,0);
//					}
//				}

				return FDOConnection;
        	}
        	catch (OSGeo.FDO.Common.Exception ex)
        	{
        		Console.WriteLine(ex.Message);
                Console.ReadLine();
                return null;
        	}
		}        

        public string ConnStr
        { get { return connStr; } }

        public DataTable ListSchema()
        {
            OSGeo.FDO.Connections.IConnection conn = connect();
            ConState = conn.Open();
            try
            {
            	if (null != conn && ConState == OSGeo.FDO.Connections.ConnectionState.ConnectionState_Open)
        		{
	            	// Get Schema Names:
					OSGeo.FDO.Commands.Schema.IGetSchemaNames pGSN =(OSGeo.FDO.Commands.Schema.IGetSchemaNames)conn.CreateCommand
						(OSGeo.FDO.Commands.CommandType.CommandType_GetSchemaNames);

					OSGeo.FDO.Common.StringCollection stCol = pGSN.Execute();

	            	dataTable = new DataTable();
	            	dataTable.Columns.Add("FeatureSchemaName", typeof(string));

					foreach ( OSGeo.FDO.Common.StringElement s in stCol)
					{
						dataTable.Rows.Add(s.String);
					}
					// sort dataTable
					DataView v = dataTable.DefaultView;
					v.Sort = "FeatureSchemaName ASC";
					dataTable = v.ToTable();

	                conn.Close();
	                return dataTable;
            	}
            	else {return null;}
            }
            catch (OSGeo.FDO.Common.Exception ex)
            {
                Console.WriteLine(ex.Message);
                Console.ReadLine();
                return null;
            }
        }
        public DataTable ListTablesOfDB(string _schemaName)
        {
            OSGeo.FDO.Connections.IConnection conn = connect();
            ConState = conn.Open();
            try
            {
            	if (null != conn && ConState == OSGeo.FDO.Connections.ConnectionState.ConnectionState_Open)
        		{
	                //Get a list of tables in the DB
					OSGeo.FDO.Commands.Schema.IGetClassNames pGSN =(OSGeo.FDO.Commands.Schema.IGetClassNames)conn.CreateCommand
						(OSGeo.FDO.Commands.CommandType.CommandType_GetClassNames);

					OSGeo.FDO.Common.StringCollection stCol = pGSN.Execute();

	            	dataTable = new DataTable();
	            	dataTable.Columns.Add("FeatureClasses", typeof(string));

					foreach ( OSGeo.FDO.Common.StringElement s in stCol)
					{
						if (s.String.StartsWith(_schemaName))
						{
							string newStr = s.String.Replace(_schemaName + ':', string.Empty);
						}
					}

					// sort dataTable
					DataView v = dataTable.DefaultView;
					v.Sort = "FeatureClasses ASC";
					dataTable = v.ToTable();

	                conn.Close();
	                return dataTable;
            	}
            	else {return null;}
            }
            catch (OSGeo.FDO.Common.Exception ex)
            {
                Console.WriteLine(ex.Message);
                Console.ReadLine();
                return null;
            }
            finally { conn.Close(); }
            }

        public string GetGeomColumn(string _tableName, string _schemaName)
        {
            string geomCol = "";
            OSGeo.FDO.Connections.IConnection conn = connect();
            ConState = conn.Open();
            try
            {
            	if (null != conn && ConState == OSGeo.FDO.Connections.ConnectionState.ConnectionState_Open)
            	{
					ISelect sel = (ISelect)conn.CreateCommand(OSGeo.FDO.Commands.CommandType.CommandType_Select);
					sel.SetFeatureClassName(_tableName);

					IFeatureReader FDOReader = sel.Execute();
					OSGeo.FDO.Schema.ClassDefinition cDef = FDOReader.GetClassDefinition();

					foreach (OSGeo.FDO.Schema.PropertyDefinition def in cDef.Properties)
					{
						if (def.PropertyType == OSGeo.FDO.Schema.PropertyType.PropertyType_GeometricProperty)
						{
							geomCol = def.Name;
						}
					}
	                conn.Close();
	                return geomCol;
            	}
            	else {return null;}
            }
            catch (OSGeo.FDO.Common.Exception ex)
            {
                Console.WriteLine(ex.Message);
                Console.ReadLine();
                return null;
            }
            finally { conn.Close(); }

        }
		public FeatureSet GetAllFeatures(string _featureClassName, string _geomColumn)
        {
            OSGeo.FDO.Connections.IConnection conn = connect();
            ConState = conn.Open();

            if (ConState == OSGeo.FDO.Connections.ConnectionState.ConnectionState_Open
                && _featureClassName != null && (_geomColumn != null || _geomColumn != ""))
            {
                ISelect sel = (ISelect)conn.CreateCommand(OSGeo.FDO.Commands.CommandType.CommandType_Select);
                sel.SetFeatureClassName(_featureClassName);

                IFeatureReader FDOReader = sel.Execute();

                GeometryCollection Geo_Collection = new GeometryCollection();
                FgfGeometryFactory fdoGeoFac = new FgfGeometryFactory();

                OSGeo.FDO.Schema.ClassDefinition cDef = FDOReader.GetClassDefinition();

                DotSpatial.Data.FeatureSet fs = new FeatureSet();

                //Populate schema of DataTable
                foreach (OSGeo.FDO.Schema.PropertyDefinition pDef in cDef.Properties)
                {
                    // Only Data - no geometry!
                    if (OSGeo.FDO.Schema.PropertyType.PropertyType_DataProperty == pDef.PropertyType)
                    {
                        //Get Datatype
                        var dProDef = pDef as OSGeo.FDO.Schema.DataPropertyDefinition;
                        OSGeo.FDO.Schema.DataType typ = dProDef.DataType;
                        Type t = ChangeType(typ);
                        //Get Property name
                        string propertyName = pDef.Name;
                        fs.DataTable.Columns.Add(pDef.Name, t);
                    }
                }

                while (FDOReader.ReadNext())
                {
                    Feature feat = new Feature();
                    DataRow dr = fs.DataTable.NewRow();
                    foreach (PropertyDefinition pDef in cDef.Properties)
                    {
                        if (pDef.PropertyType == PropertyType.PropertyType_DataProperty)
                        {
                            DataPropertyDefinition dDef = pDef as DataPropertyDefinition;
                            switch (dDef.DataType)
                            {
                                case OSGeo.FDO.Schema.DataType.DataType_String:
                                    if (!FDOReader.IsNull(dDef.Name))
                                        dr[dDef.Name] = FDOReader.GetString(dDef.Name);
                                    break;
                                case OSGeo.FDO.Schema.DataType.DataType_Int16:
                                    if (!FDOReader.IsNull(dDef.Name))
                                        dr[dDef.Name] = FDOReader.GetInt16(dDef.Name);
                                    break;
                                case OSGeo.FDO.Schema.DataType.DataType_Int32:
                                    if (!FDOReader.IsNull(dDef.Name))
                                        dr[dDef.Name] = FDOReader.GetInt32(dDef.Name);
                                    break;
                                case OSGeo.FDO.Schema.DataType.DataType_Int64:
                                    if (!FDOReader.IsNull(dDef.Name))
                                        dr[dDef.Name] = FDOReader.GetInt64(dDef.Name);
                                    break;
                                case OSGeo.FDO.Schema.DataType.DataType_DateTime:
                                    if (!FDOReader.IsNull(dDef.Name))
                                        dr[dDef.Name] = FDOReader.GetDateTime(dDef.Name);
                                    break;
                                case OSGeo.FDO.Schema.DataType.DataType_Single:
                                    if (!FDOReader.IsNull(dDef.Name))
                                        dr[dDef.Name] = FDOReader.GetSingle(dDef.Name);
                                    break;
                                case OSGeo.FDO.Schema.DataType.DataType_Double:
                                    if (!FDOReader.IsNull(dDef.Name))
                                        dr[dDef.Name] = FDOReader.GetDouble(dDef.Name);
                                    break;
                                case OSGeo.FDO.Schema.DataType.DataType_Decimal:
                                    if (!FDOReader.IsNull(dDef.Name))
                                        dr[dDef.Name] = FDOReader.GetDouble(dDef.Name);
                                    break;
                                case OSGeo.FDO.Schema.DataType.DataType_Boolean:
                                    if (!FDOReader.IsNull(dDef.Name))
                                        dr[dDef.Name] = FDOReader.GetBoolean(dDef.Name);
                                    break;
                                default:
                                    if (!FDOReader.IsNull(dDef.Name))
                                        dr[dDef.Name] = FDOReader.GetByte(dDef.Name);
                                    break;
                            }
                            feat.DataRow = dr;
                        }

                        if (pDef.PropertyType == PropertyType.PropertyType_GeometricProperty)
                        {
                            //Get Geometry with FDO Reader
                            Byte[] Tmppts = FDOReader.GetGeometry(_geomColumn);
                            IGeometry fdoGeo = fdoGeoFac.CreateGeometryFromFgf(Tmppts);
                            // Convert to WKB
                            Byte[] wkbFDO = fdoGeoFac.GetWkb(fdoGeo);

                            // Read WKB from FDO and convert to DotSpatial Geometry
                            DotSpatial.Topology.GeometryFactory geoFac = new DotSpatial.Topology.GeometryFactory();
                            DotSpatial.Topology.Utilities.WkbReader wkbReader = new DotSpatial.Topology.Utilities.WkbReader();
                            DotSpatial.Topology.IGeometry geom = wkbReader.Read(wkbFDO);

                            //Add DotSpatial Geometry
                            feat.BasicGeometry = geom;
                        }
                    }
                    fs.Features.Add(feat);
                }
                return fs;
            }
            else {return null;}
        }

        public static Type ChangeType(OSGeo.FDO.Schema.DataType dt)
		{

			switch (dt)
			{
				case OSGeo.FDO.Schema.DataType.DataType_BLOB:
				{
					return Type.GetType("Sytem.Object");
				}
				case OSGeo.FDO.Schema.DataType.DataType_Boolean:
				{
					return Type.GetType("System.Boolean");
				}
				case OSGeo.FDO.Schema.DataType.DataType_Byte:
				{
					return Type.GetType("System.Byte");
				}
				case OSGeo.FDO.Schema.DataType.DataType_Int16:
				{
					return Type.GetType("System.Int16");
				}
				case OSGeo.FDO.Schema.DataType.DataType_Int32:
				{
					return Type.GetType("System.Int32");
				}
				case OSGeo.FDO.Schema.DataType.DataType_Int64:
				{
					return Type.GetType("System.Int64");
				}
				case OSGeo.FDO.Schema.DataType.DataType_Single:
				{
					return Type.GetType("System.Single");
				}
				case OSGeo.FDO.Schema.DataType.DataType_Double:
				{
					return Type.GetType("System.Double");
				}
				case OSGeo.FDO.Schema.DataType.DataType_Decimal:
				{
					return Type.GetType("System.Decimal");
				}
				case OSGeo.FDO.Schema.DataType.DataType_DateTime:
				{
					return Type.GetType("System.DateTime");
				}
				case OSGeo.FDO.Schema.DataType.DataType_String:
				{
					return Type.GetType("System.String");
				}
			}
		throw new ArgumentException("Unknown DataType");
		}
	}
}

4 Leute mögen diesen Artikel.
Mai 29th, 2011

Feature Data Object API und DotSpatial in .NET (3)

Die Agenda beim Thema FDO in .NET sah folgende Punkte vor:

  1. Get the list of installed providers
  2. Create a connection manager
  3. Create a connection
  4. Set the connection properties
  5. Open a connection
  6. Get the connection state
  7. Fetch Data

Die Punkte 1-6 wurden bereits im letzten Artikel abgehandelt. Wir werden uns nun dem 7. Punkt widmen. Und der hat es in sich. Denn wenn man die FDO API zum Laden von Daten verwendet, bekommt man FDO Objekte. Ziel soll es aber sein, die FDO Objekte in einem DotSpatial Viewer darzustellen.

Dazu muss man das Ergebnis der FDO API zu einem DotSpatial FeatureSet zusammenbauen.

7. Fetch Data

Zunächst müssen wir ein Command erzeugen, das wir später dann ausführen werden. (Eine Liste der verfügbaren Commands ist im Namespace OSGeo.FDO.Commands.CommandType zu finden. Außerdem sind die Commands auch immer abhängig vom Datenprovider. Die Dokumentation der FDO hilft hier weiter.)

if (connState == OSGeo.FDO.Connections.ConnectionState.ConnectionState_Open)
{
	ISelect sel = (ISelect)conn.CreateCommand(OSGeo.FDO.Commands.CommandType.CommandType_Select);
    sel.SetFeatureClassName("world_0");
	IFeatureReader reader = sel.Execute();
	FgfGeometryFactory geoFac = new FgfGeometryFactory();

Wir erzeugen also ein Select-Command, mit dem man die Daten einer Datenquelle abfragen kann. Dann weisen wir noch dem Select-Command den Namen der FeatureClass zu, die wir abfragen wollen. Im Falle des Shapefiles ist es der blanke Dateiname ohne die Endung “.shp”. Da wir keinen Filter eingebaut haben, werden im nächsten Befehl alle Daten per FeatureReader zurückgegeben.
Außerdem instanziieren wir eine GeometryFactory, die wir später noch benötigen werden.

	//FDO Get Feature Class Defintion (Attribut schema)
	DataTable dt = new DataTable();
	ClassDefinition cDef = reader.GetClassDefinition();
	foreach (PropertyDefinition pDef in cDef.Properties)
	{
		if (pDef.PropertyType == PropertyType.PropertyType_DataProperty)
		{
			DataPropertyDefinition dDef = pDef as DataPropertyDefinition;
			// Get .NET Datatype because of FDO's Enum of datatypes
			Type datatype = GetNETDataType(dDef);
			if (!dt.Columns.Contains(dDef.Name))
				dt.Columns.Add(dDef.Name, datatype);
		}
	}

Im oben aufgezeigten Code-Block wird für das Attributschema der zu erzeugenden FeatureClass zuerst eine DataTable erzeugt und mit den entsprechenden Columns und den zugehörigen Datentypen versehen. Da die FDO API eigene Enumerations für die Datentypen einer Class (FeatureClass oder Tabelle) zurückgibt, für die Definition eines Attributschemas der DataTable jedoch .NET Datentypen gebraucht werden, habe ich eine Hilfsfunktion (GetNETDataType) geschrieben, die eben für ein Element aus dem Enum von FDO einen .NET Datentyp zurückgibt. Den Code dieser Funktion findet man ganz unten auf dieser Seite.

Weiter gehts mit der Instanziierung eines FeatureSets. Ein FeatureSet entspricht in der Terminologie von DotSpatial einer Sammlung von Features. Das Äquivalent der FDO API oder auch von ESRI wäre hier eine FeatureClass. Da wir bereits eine DataTable mit dem passenden Attributschema des Shapefiles aus der FDO API haben, können wir die Struktur der DataTable mit einer Methode dem FeatureSet übergeben.

	DotSpatial.Data.FeatureSet fs = new DotSpatial.Data.FeatureSet();
	fs.CopyTableSchema(dt);

Jetzt holen wir uns die Attributdaten und die Geometry per FDO-FeatureReader aus dem Datensatz. Für jede Datenzeile erzeugen wir ein neues DotSpatial-Feature, das später dann ins FeatureSet wandert. Anschließend gehe ich in einer foreach-Schleife die FDO PropertyDefinition durch, was im Endeffekt einem Attributschema entspricht. Dabei kann man mit der FDO API unterscheiden, ob es eine DataProperty oder eine GeometryProperty ist.

Dann kommt eine etwas unschöne switch-case-Anweisung auf die ich nicht Stolz bin, aber es geht meines Wissens mit der FDO API nicht anders. Man muss beim FeatureReader der FDO wissen, welcher Datentyp dem Attribut zu Grunde liegt, um es mit der entsprechenden Methode (z.B.: reader.GetString()) korrekt auslesen zu können. Am Besten versteht man das im Code. Sind alle Attribute durchlaufen und die entsprechenden Attributwerte ausgelesen, wird die DataRow an die DataRow des DotSpatial-Features gehängt.

	//FDO Get Data & Geometry
	while (reader.ReadNext())
	{
		DataRow dr = dt.NewRow();
		DotSpatial.Data.Feature feature = new DotSpatial.Data.Feature();

		foreach (PropertyDefinition pDef in cDef.Properties)
		{
			if (pDef.PropertyType == PropertyType.PropertyType_DataProperty)
			{
				DataPropertyDefinition dDef = pDef as DataPropertyDefinition;
				switch (dDef.DataType.ToString())
				{
					case "DataType_String":
						if (!reader.IsNull(dDef.Name))
							dr[dDef.Name] = reader.GetString(dDef.Name);
						break;
					case "DataType_Int16":
						if (!reader.IsNull(dDef.Name))
							dr[dDef.Name] = reader.GetInt16(dDef.Name);
						break;
					case "DataType_Int32":
						if (!reader.IsNull(dDef.Name))
							dr[dDef.Name] = reader.GetInt32(dDef.Name);
						break;
					case "DataType_Int64":
						if (!reader.IsNull(dDef.Name))
							dr[dDef.Name] = reader.GetInt64(dDef.Name);
						break;
					case "DataType_DateTime":
						if (!reader.IsNull(dDef.Name))
							dr[dDef.Name] = reader.GetDateTime(dDef.Name);
						break;
					case "DataType_Single":
						if (!reader.IsNull(dDef.Name))
							dr[dDef.Name] = reader.GetSingle(dDef.Name);
						break;
					case "DataType_Double":
						if (!reader.IsNull(dDef.Name))
							dr[dDef.Name] = reader.GetDouble(dDef.Name);
						break;
					case "DataType_Decimal":
						if (!reader.IsNull(dDef.Name))
							dr[dDef.Name] = reader.GetDouble(dDef.Name);
						break;
					case "DataType_Boolean":
						if (!reader.IsNull(dDef.Name))
							dr[dDef.Name] = reader.GetBoolean(dDef.Name);
						break;
					default:
						if (!reader.IsNull(dDef.Name))
							dr[dDef.Name] = reader.GetByte(dDef.Name);
						break;
				}
				feature.DataRow = dr;
			}

Beim Auslesen der Geometrie wirds auch nochmal spannend. Hier wird per FDO API die Geometrie binär ausgelesen und in eine FDO-Geometrie mit Hilfe der anfangs erzeugten GeometryFactory verwandelt. Diese Bytes sind aber eben nicht standardmäßig vom Typ Well-Known-Binary (WKB). Das muss man erst noch einmal aus einer FDO-Geometrie erzeugen. WKB ist deswegen so wichtig, weil es der kleinste gemeinsame Nenner beim Austausch der Geometrie zwischen der FDO API und DotSpatial ist. Hab ich also eine WKB-Geometrie kann ich sie mit dem enstprechenden WKBReader von DotSpatial in eine DotSpatial-Geometrie verwandeln und dem Feature zuweisen. Dann bin ich mit den Attributdaten und der Geometrie durch und muss das Feature dem FeatureSet hinzufügen.

Achtung: Wichtig ist hier noch, dass man nicht die Methode fs.AddFeature() verwendet. Diese akzeptiert auch Features - unterschlägt jedoch die Attributdaten des übergebenen Features.

		if (pDef.PropertyType == PropertyType.PropertyType_GeometricProperty)
		{
			//Get Raw Binary Data and convert it to WKB-DotSpatial
			Byte[] bytes = reader.GetGeometry("Geometry");
			IGeometry geom = geoFac.CreateGeometryFromFgf(bytes);
			Byte[] wkb = geoFac.GetWkb(geom);

			DotSpatial.Data.WKBReader wkbReader = new DotSpatial.Data.WKBReader();
			DotSpatial.Topology.IGeometry dSgeom = wkbReader.Read(wkb);
			feature.BasicGeometry = dSgeom;
		}
	}
	fs.Features.Add(feature);
}
//TODO: Add DotSpatial FeatureSet to map
//for example:
//fs.Name = "test";
//this.map1.Layers.Add(fs);
//this.map1.ZoomToExtents();

Hier noch die Übersetzermethode für die FDO-Datentypen in .NET-Datentypen.

/// <summary>
/// Method for getting the .NET DataType from FDO DataPropertyDefintion
/// </summary>
/// <param name="dDef">FDO DataPropertyDefinition</param>
/// <returns>.NET DataType</returns>
public Type GetNETDataType(OSGeo.FDO.Schema.DataPropertyDefinition dDef)
{
    switch (dDef.DataType.ToString())
    {
	case "DataType_String":
	    return System.Type.GetType("System.String");
	case "DataType_Int16":
	    return System.Type.GetType("System.Int16");
	case "DataType_Int32":
	    return System.Type.GetType("System.Int32");
	case "DataType_Int64":
	    return System.Type.GetType("System.Int64");
	case "DataType_DateTime":
	    return System.Type.GetType("System.DateTime");
	case "DataType_Single":
	    return System.Type.GetType("System.Single");
	case "DataType_Double":
	    return System.Type.GetType("System.Double");
	case "DataType_Decimal":
	    return System.Type.GetType("System.Decimal");
	case "DataType_Boolean":
	    return System.Type.GetType("System.Boolean");
	default:
	    return System.Type.GetType("System.Byte");
    }
}

Soweit die Verwendung der FDO API in .NET für DotSpatial. Ich habe das Ganze anhand eines Shapefiles durchgespielt, was natürlich nicht notwendig wäre, weil DotSpatial ESRI Shapefiles bereits liest. Interessant wird die FDO API aber dann bei OGC Webservices, GDAL/OGR und vor allem bei Oracle Spatial und der ArcSDE. Und um die Verwendung der FDO API bei der ArcSDE soll es im nachfolgenden Artikel gehen..

Be the first to like.
Mai 29th, 2011

Feature Data Object API und DotSpatial in .NET (2)

Ich möchte zunächst einmal eine Verbindung zu einem ESRI Shapefile mit der FDO API erstellen. Die Verbindung zu einer ESRI ArcSDE mit der FDO werde ich später aufzeigen.
Die Dokumentation von FDO gibt unter der Rubrik “Getting Started” folgende Hinweise:

Write the Code to Connect to a Provider

Do the following:

  1. Get the list of installed providers
  2. Create a connection manager
  3. Create a connection
  4. Get the connection state
  5. Get the connection properties
  6. Get values for the connection properties
  7. Set the connection properties
  8. Open a connection
  9. Open a pending connection

Diese Auflistung werde ich etwas verkürzen. Die Stufen 5-6 kann man nämlich auch mit Hilfe der Dokumentation lösen. Interessant ist jedoch, dass man auch per Code die jeweiligen Verbindungseinstellungen und Möglichkeiten abrufen könnte.

Meine Agenda sieht folgendes vor:

  1. Get the list of installed providers
  2. Create a connection manager
  3. Create a connection
  4. Set the connection properties
  5. Open a connection
  6. Get the connection state
  7. Fetch Data

Und das wollen wir nun mit ein paar Code-Schnippsel füllen.

1. Get the list of installed providers

Mit den folgenden Zeilen kann man sich alle registrierten Provider anzeigen lassen.

//FDO Provider Registry
IProviderRegistry FDORegistry = (IProviderRegistry)FeatureAccessManager.GetProviderRegistry();
ProviderCollection providers = FDORegistry.GetProviders();
string s = "Registered FDO Providers: n";
foreach (Provider p in providers)
{
	s += "# " + p.DisplayName + "n";
}
MessageBox.Show(s, "Registered FDO providers", MessageBoxButtons.OK, MessageBoxIcon.Information);

Hat alles geklappt, sollte man folgende MessageBox sehen:

Achtung: Dabei werden die Provider ausgegeben, wie sie in der Datei providers.xml aufgelistet sind. Das bedeutet jedoch nicht, dass alle Provider und die Assemblies dafür auch wirklich vorhanden sind. Die Provider-Registry liest also nur das XML aus, ohne zu prüfen, ob die referenzierten DLLs auch existieren.

 

2. Create a connection manager / 3. Create a connection / 4. Set the connection properties / 5. Open a connection

Wir brauchen also erst einmal einen ConnectionManager. Mit diesem erzeugen wir eine Verbindung mit der Angabe des entsprechenden Datenproviders als string, z.B. "OSGeo.SHP.3.6".

//FDO Connection - ShapeFile example
IConnectionManager connManager = FeatureAccessManager.GetConnectionManager();
IConnection conn = connManager.CreateConnection("OSGeo.SHP.3.6");
string shapeFile = @"D:\GISDATA\world_adm0.shp";
conn.ConnectionInfo.ConnectionProperties.SetProperty("DefaultFileLocation", shapeFile);
OSGeo.FDO.Connections.ConnectionState connState = conn.Open();

Mit der Wahl des Datenproviders ändern sich die Möglichkeiten, die man bei den Verbindungseigenschaften (ConnectionProperties) setzen kann. Eine Liste der Eigenschaften, die je nach Provider gesetzt werden müssen bzw. können findet man in der FDO Doku – The Open Source Providers ->Connection API. Bei ESRI Shapefile muss man mindestens "DefaultFileLocation" oder "TemporaryFileLocation" und den Pfad zum Shapefile angeben (s.o.).

In der letzten Zeile des obigen Code-Blocks merkt man spätestens beim Aufruf der Methode Open(), ob die benötigten Datenprovider auch wirklich an der Stelle sind, wie sie in der Datei providers.xml beschrieben wurden. Erst dann werden die DLLs samt Abhängigkeiten nämlich zur Laufzeit geladen.

6. Get the connection state

Den Status der Verbindung sollte man abrufen, um eine Fehlerquelle auszuschließen. Das geht auch recht einfach. Anschließend werden wir Daten aus der Datenquelle holen.

//FDO Check ConnectionState
if (connState == OSGeo.FDO.Connections.ConnectionState.ConnectionState_Open)
{
	//Fetch Data
}

Wie man nun aus der FDO-Verbindung Daten herausbekommt (Punkt 7. Fetch Data) bespreche ich im dritten Teil

1 andere Person mag diesen Artikel.
Mai 29th, 2011

Feature Data Object API und DotSpatial in .NET (1)

DotSpatial

DotSpatial ist eine sehr gut dokumentierte, quelloffene GIS – Bibliothek für .NET 4. In diesem Projekt sind einige Open Source basierte GIS-Bibliotheken zusammen geflossen – unter anderem MapWindow6, ProjNET oder GPS.NET3. Auch Entwickler von SharpMap sind bei dem groß angelegten Projekt dabei. Gute Tutorials zu DotSpatial für den Einstieg findet man hier. SharpMap ist zwar ganz toll – es existiert ja auch schon länger für .NET. Aber DotSpatial ist wirklich der Hammer!

Project Vision: DotSpatial aims to provide a free, open source, consistent and dependable set of libraries for the .NET, Silverlight and Mono platforms, enabling developers to easily incorporate spatial data, analysis, and mapping into their applications thereby unleashing the massive potential of GIS in solutions for organizations and communities of all types in a nonrestrictive way.

DotSpatial Architektur

Mit DotSpatial kann man – ohne viel Programmierkenntnisse zu besitzen – ein kleines GIS zusammenbasteln. Mit der entsprechenden Entwicklungsumgebung (MS Visual Studio Express oder SharpDevelop) kommt man schon mit Drag&Drop so weit, dass man die meisten Funktionen von DotSpatial nutzen kann. Bei DotSpatial werden die entsprechenden GIS-Controls mitgeliefert – so wie man es vielleicht von anderen kommerziellen GIS-Bibliotheken kennt. Man merkt schon: Ich bin begeistert..

Im aktuellen Release von DotSpatial kann die Bibliothek mehrere Geodatenformate (sowohl Vektor- als auch Rasterformate) lesen. Schreiben kann es auf jeden Fall ESRI Shapefiles. Seit kurzem gibt es auch einen experimentellen lesenden Datenprovider für PostGIS und SpatiaLite. Schöne Sache – aber es gibt ja noch mehr Geodatenbanken bzw. Middleware auf diesen.  Ich wollte also lesenden Zugriff auf die ESRI ArcSDE in einem eigenen GIS-Viewer realisieren.

Nach etwas Recherche kam ich auf die Feature Data Object API.

Feature Data Object API

Die Feature Data Object API ist ein OSGeo-Projekt, das ursprünglich von Autodesk entwickelt wurde. Es stellt ein abstraktes Objektmodell für Geodaten dar, das viele Geodatenformate lesen und schreiben kann.

FDO Data Access Technology is an API for manipulating, defining and analyzing geospatial information regardless of where it is stored. FDO uses a provider-based model for supporting a variety of geospatial data sources, where each provider typically supports a particular data format or data store. FDO (“Feature Data Object”) is free, open source software licensed under the LGPL.

Feature Data Objects ArchitekturUnter anderem liest diese Bibliothek ESRI ArcSDE, OGR/GDAL, OGC Web Services, MySQL und mit einem proprietären Provider auch Oracle Spatial. Eine vollständige Liste und die Möglichkeiten der Provider findet man hier.

Interessanterweise setzt auch safe die FDO API in ihrer FME-Software ein. Auf jeden Fall ist die Bibliothek Open Source und sowohl in C++ als auch in .NET einsetzbar. Eine GUI für die API ist die FDO Toolbox.

So wunderbar die Dokumentation von DotSpatial für den Anfang auch ist – bei FDO fuer .NET muss man schon länger suchen und einiges ausprobieren. Zwar gibt es einiges an Doku auf der Seite der FDO. Allerdings fehlten mir praktische Beispiele für den schnellen Einstieg. Hilfreich war aber dieses Tutorial:

“Taking Geospatial Data Access to the next level with the FDO API”

Voraussetzungen für die Verwendung der FDO API in .NET

Im Ausgabeverzeichnis (bin\Debug oder bin\Release) des Visual Studio-Projekts müssen folgende Assemblies von FDO liegen:

  • Fdo.dll
  • FDOCommon.dll
  • FDOGeometry.dll
  • FDOMessages.dll
  • FDOSpatial.dll
  • OSGeo.FDO.dll
  • OSGeo.FDO.Common.dll
  • OSGeo.FDO.Geometry.dll
  • Xalan-C_1_11.dll
  • XalanMessages_1_11.dll
  • xerces-c_3_1.dll

Direkt arbeitet man in .NET nur mit den Assemblies im Namespace OSGeo, die dem entsprechend auch im Projekt referenziert werden müssen. Die anderen oben aufgeführten DLLs werden von diesen Assemblies jedoch auch im selben Verzeichnis benötigt. Darüber hinaus existieren noch mehr Assemblies der Provider für die FDO, die jedoch nicht im Ausgabeverzeichnis sein müssen. In der Datei providers.xml werden die Datenprovider und der Pfad zu diesen DLLs und den weiteren abhängigen DLLs angegeben. Dieses XML sollte den Bedürfnissen (v.a. beim Tag LibraryPath) entsprechend angepasst werden und auch im Ausgabepfad der Anwendung gespeichert sein.

Eine mögliche Konfiguration ist dabei, dass die Core-Assemblies (s.o.) und die Datei providers.xml im Ausgabeverzeichnis sind und die eigentlichen Datenprovider mit deren Abhängigkeiten in einem Unterverzeichnis des Ausgabepfads. Im Beispiel oben habe ich einen Unterordner FDO_3.6.0 angelegt.

Außerdem sollte man gleich alle Namespaces von FDO per using-Direktive ins eigene Projekt einbinden:

//FDO
using OSGeo.FDO;
using OSGeo.FDO.Connections;
using OSGeo.FDO.Common;
using OSGeo.FDO.Schema;
using OSGeo.FDO.Commands;
using OSGeo.FDO.Geometry;
using OSGeo.FDO.ClientServices;
using OSGeo.FDO.Commands.DataStore;
using OSGeo.FDO.Commands.Schema;
using OSGeo.FDO.Expression;
using OSGeo.FDO.Commands.Feature;
using OSGeo.FDO.Filter;

Weiter gehts im nächsten Teil..

3 Leute mögen diesen Artikel.
März 13th, 2011

G#.Viewer – Kartenfunktionen (4)

Im letzten Teil der Serie beschäftigten wir uns mit dem Laden und Anzeigen von ESRI Shapefiles im G#.Viewer. Nun werden wir die einfach zugänglichen Funktionen zum Bedienen der Kartenansicht in unseren GIS-Viewer einbauen.

Im Namespace SharpMap.Forms.MapImage befindet sich ein Enumerator, der die verfügbaren Kartenfunktionalitäten auflistet.

Die wichtigsten sind:

  • Pan (Verschieben)
  • ZoomIn
  • ZoomOut
  • Query

Da diese Tools ja bereits “out-of-the-box” mitgeliefert werden, brauchen wir nichts anderes tun, als an jedem LabelButton in unserem ToolStrip ein Click-Event zu erzeugen und den Wechsel des aktuellen Werkzeugs zu vollziehen. Was sich etwas umständlich anhört ist mit C# und SharpDevelop ganz simpel umzusetzen:

1 andere Person mag diesen Artikel.

Switch to our mobile site