Skill Tracer
Skills › Scientific Agent Skills › geomaster › gis-software.md

gis-software.md

325 lines · skills/geomaster/references/gis-software.md at 5f2bc03

Open on GitHub

Most marks come from matching words in the file, so treat them as pointers.

1# GIS Software Integration
2
3Native GIS integration, reviewed 2026-10-01. All examples are illustrative: these
4applications/licenses were not installed or executed. Run them in the GIS vendor's
5Python environment, check the installed version and inspect tool help before use.
6QGIS's `latest` cookbook currently redirects to 3.44, so use explicit 4.2 material
7when targeting its Qt6 runtime.
8

GIS Software Integration

Native GIS integration, reviewed 2026-10-01. All examples are illustrative: these applications/licenses were not installed or executed. Run them in the GIS vendor's Python environment, check the installed version and inspect tool help before use. QGIS's latest cookbook currently redirects to 3.44, so use explicit 4.2 material when targeting its Qt6 runtime.

Raw lines 1 to 8
9## QGIS / PyQGIS

QGIS / PyQGIS

Raw lines 9 to 10
11### Running Python Scripts in QGIS
13```python
14# Processing framework script
15from qgis.core import (QgsProject, QgsVectorLayer, QgsRasterLayer,
16 QgsProcessingAlgorithm, QgsProcessingParameterRasterLayer)
18# Load layers
19vector_layer = QgsVectorLayer("path/to/shapefile.shp", "layer_name", "ogr")
20raster_layer = QgsRasterLayer("path/to/raster.tif", "raster_name", "gdal")
22# Add to project
23QgsProject.instance().addMapLayer(vector_layer)
24QgsProject.instance().addMapLayer(raster_layer)
26# Access features
27for feature in vector_layer.getFeatures():
28 geom = feature.geometry()
29 attrs = feature.attributes()
30```

Running Python Scripts in QGIS

# Processing framework script
from qgis.core import (QgsProject, QgsVectorLayer, QgsRasterLayer,
                       QgsProcessingAlgorithm, QgsProcessingParameterRasterLayer)

# Load layers
vector_layer = QgsVectorLayer("path/to/shapefile.shp", "layer_name", "ogr")
raster_layer = QgsRasterLayer("path/to/raster.tif", "raster_name", "gdal")

# Add to project
QgsProject.instance().addMapLayer(vector_layer)
QgsProject.instance().addMapLayer(raster_layer)

# Access features
for feature in vector_layer.getFeatures():
    geom = feature.geometry()
    attrs = feature.attributes()
Raw lines 11 to 31
32### Processing algorithms
34Run inside an initialized QGIS environment with Processing registered. Inspect the
35provider's current parameters before assembling a workflow:
37```python
38import processing
39processing.algorithmHelp('native:buffer')
40result = processing.run('native:buffer', {
41 'INPUT': metric_vector_layer, 'DISTANCE': 100, 'SEGMENTS': 8,
42 'END_CAP_STYLE': 0, 'JOIN_STYLE': 0, 'MITER_LIMIT': 2,
43 'DISSOLVE': False, 'OUTPUT': 'TEMPORARY_OUTPUT',
44})
45buffer_layer = result['OUTPUT']
46```
48For a custom `QgsProcessingAlgorithm`, implement parameter registration,
49`processAlgorithm` and `createInstance`, and return the actual produced destination.
50A placeholder `destination` variable is not an NDVI implementation. Raster calculations
51must use explicit bands, scale/offset, masks and a defined output grid as in the local
52raster helper; wire those through registered parameters rather than guessing Sentinel
53band positions. Validate `QgsVectorLayer.isValid()`/`QgsRasterLayer.isValid()` before
54adding or processing a layer.

Processing algorithms

Run inside an initialized QGIS environment with Processing registered. Inspect the provider's current parameters before assembling a workflow:

import processing
processing.algorithmHelp('native:buffer')
result = processing.run('native:buffer', {
    'INPUT': metric_vector_layer, 'DISTANCE': 100, 'SEGMENTS': 8,
    'END_CAP_STYLE': 0, 'JOIN_STYLE': 0, 'MITER_LIMIT': 2,
    'DISSOLVE': False, 'OUTPUT': 'TEMPORARY_OUTPUT',
})
buffer_layer = result['OUTPUT']

For a custom QgsProcessingAlgorithm, implement parameter registration, processAlgorithm and createInstance, and return the actual produced destination. A placeholder destination variable is not an NDVI implementation. Raster calculations must use explicit bands, scale/offset, masks and a defined output grid as in the local raster helper; wire those through registered parameters rather than guessing Sentinel band positions. Validate QgsVectorLayer.isValid()/QgsRasterLayer.isValid() before adding or processing a layer.

Raw lines 32 to 55
56### Plugin Development
58```python
59# __init__.py
60def classFactory(iface):
61 from .my_plugin import MyPlugin
62 return MyPlugin(iface)
64# my_plugin.py
65from qgis.PyQt.QtCore import QSettings
66from qgis.PyQt.QtGui import QAction # QGIS 4 / Qt6
67from qgis.core import QgsProject
69class MyPlugin:
70 def __init__(self, iface):
71 self.iface = iface
73 def initGui(self):
74 self.action = QAction("My Plugin", self.iface.mainWindow())
75 self.action.triggered.connect(self.run)
76 self.iface.addPluginToMenu("My Plugin", self.action)
78 def run(self):
79 # Plugin logic here
80 pass
82 def unload(self):
83 self.iface.removePluginMenu("My Plugin", self.action)
84```

Plugin Development

# __init__.py
def classFactory(iface):
    from .my_plugin import MyPlugin
    return MyPlugin(iface)

# my_plugin.py
from qgis.PyQt.QtCore import QSettings
from qgis.PyQt.QtGui import QAction  # QGIS 4 / Qt6
from qgis.core import QgsProject

class MyPlugin:
    def __init__(self, iface):
        self.iface = iface

    def initGui(self):
        self.action = QAction("My Plugin", self.iface.mainWindow())
        self.action.triggered.connect(self.run)
        self.iface.addPluginToMenu("My Plugin", self.action)

    def run(self):
        # Plugin logic here
        pass

    def unload(self):
        self.iface.removePluginMenu("My Plugin", self.action)
Raw lines 56 to 85
86## ArcGIS / ArcPy

ArcGIS / ArcPy

Raw lines 86 to 87
88### Basic ArcPy Operations
90```python
91import arcpy
93# Set workspace
94arcpy.env.workspace = "C:/data"
96# Set output overwrite
97arcpy.env.overwriteOutput = True
99# Set scratch workspace
100arcpy.env.scratchWorkspace = "C:/data/scratch"
102# List features
103feature_classes = arcpy.ListFeatureClasses()
104rasters = arcpy.ListRasters()
105```

Basic ArcPy Operations

import arcpy

# Set workspace
arcpy.env.workspace = "C:/data"

# Set output overwrite
arcpy.env.overwriteOutput = True

# Set scratch workspace
arcpy.env.scratchWorkspace = "C:/data/scratch"

# List features
feature_classes = arcpy.ListFeatureClasses()
rasters = arcpy.ListRasters()
Raw lines 88 to 106
107### Geoprocessing Workflows
109```python
110import arcpy
111from arcpy.sa import *
113# Check out Spatial Analyst extension
114arcpy.CheckOutExtension("Spatial")
116# Set environment
117arcpy.env.workspace = "C:/data"
118arcpy.env.cellSize = 10
119arcpy.env.extent = "study_area"
121# Slope analysis
122out_slope = Slope("dem.tif")
123out_slope.save("slope.tif")
125# Aspect
126out_aspect = Aspect("dem.tif")
127out_aspect.save("aspect.tif")
129# Hillshade
130out_hillshade = Hillshade("dem.tif", azimuth=315, altitude=45)
131out_hillshade.save("hillshade.tif")
133# Viewshed analysis
134out_viewshed = Viewshed("dem.tif", "observer_points.shp") # observer fields follow tool schema
135out_viewshed.save("viewshed.tif")
137# Cost distance
138cost_raster = DistanceAccumulation("source.shp", in_cost_raster="cost.tif")
139cost_raster.save("cost_distance.tif")
141# Hydrology: Flow direction
142flow_dir = FlowDirection("dem.tif")
143flow_dir.save("flowdir.tif")
145# Flow accumulation
146flow_acc = FlowAccumulation(flow_dir)
147flow_acc.save("flowacc.tif")
149# Stream delineation
150stream = Con(flow_acc > 1000, 1)
151stream_raster = StreamOrder(stream, flow_dir)
152```

Geoprocessing Workflows

import arcpy
from arcpy.sa import *

# Check out Spatial Analyst extension
arcpy.CheckOutExtension("Spatial")

# Set environment
arcpy.env.workspace = "C:/data"
arcpy.env.cellSize = 10
arcpy.env.extent = "study_area"

# Slope analysis
out_slope = Slope("dem.tif")
out_slope.save("slope.tif")

# Aspect
out_aspect = Aspect("dem.tif")
out_aspect.save("aspect.tif")

# Hillshade
out_hillshade = Hillshade("dem.tif", azimuth=315, altitude=45)
out_hillshade.save("hillshade.tif")

# Viewshed analysis
out_viewshed = Viewshed("dem.tif", "observer_points.shp")  # observer fields follow tool schema
out_viewshed.save("viewshed.tif")

# Cost distance
cost_raster = DistanceAccumulation("source.shp", in_cost_raster="cost.tif")
cost_raster.save("cost_distance.tif")

# Hydrology: Flow direction
flow_dir = FlowDirection("dem.tif")
flow_dir.save("flowdir.tif")

# Flow accumulation
flow_acc = FlowAccumulation(flow_dir)
flow_acc.save("flowacc.tif")

# Stream delineation
stream = Con(flow_acc > 1000, 1)
stream_raster = StreamOrder(stream, flow_dir)
Raw lines 107 to 153
154### Vector Analysis
156```python
157# Buffer analysis
158arcpy.Buffer_analysis("roads.shp", "roads_buffer.shp", "100 meters")
160# Spatial join
161arcpy.SpatialJoin_analysis("points.shp", "zones.shp", "points_joined.shp",
162 join_operation="JOIN_ONE_TO_ONE",
163 match_option="HAVE_THEIR_CENTER_IN")
165# Dissolve
166arcpy.Dissolve_management("parcels.shp", "parcels_dissolved.shp",
167 dissolve_field="OWNER_ID")
169# Intersect
170arcpy.Intersect_analysis(["layer1.shp", "layer2.shp"], "intersection.shp")
172# Clip
173arcpy.Clip_analysis("input.shp", "clip_boundary.shp", "output.shp")
175# Select by location
176arcpy.SelectLayerByLocation_management("points_layer", "HAVE_THEIR_CENTER_IN",
177 "polygon_layer")
179# Feature to raster
180arcpy.FeatureToRaster_conversion("landuse.shp", "LU_CODE", "landuse.tif", 10)
181```

Vector Analysis

# Buffer analysis
arcpy.Buffer_analysis("roads.shp", "roads_buffer.shp", "100 meters")

# Spatial join
arcpy.SpatialJoin_analysis("points.shp", "zones.shp", "points_joined.shp",
                           join_operation="JOIN_ONE_TO_ONE",
                           match_option="HAVE_THEIR_CENTER_IN")

# Dissolve
arcpy.Dissolve_management("parcels.shp", "parcels_dissolved.shp",
                          dissolve_field="OWNER_ID")

# Intersect
arcpy.Intersect_analysis(["layer1.shp", "layer2.shp"], "intersection.shp")

# Clip
arcpy.Clip_analysis("input.shp", "clip_boundary.shp", "output.shp")

# Select by location
arcpy.SelectLayerByLocation_management("points_layer", "HAVE_THEIR_CENTER_IN",
                                      "polygon_layer")

# Feature to raster
arcpy.FeatureToRaster_conversion("landuse.shp", "LU_CODE", "landuse.tif", 10)
Raw lines 154 to 182
183### ArcGIS Pro Notebooks
185```python
186# ArcGIS Pro Jupyter Notebook
187import arcpy
188import pandas as pd
189import matplotlib.pyplot as plt
191# Use current project's map
192aprx = arcpy.mp.ArcGISProject("CURRENT")
193m = aprx.listMaps()[0]
195# Get layer
196layer = m.listLayers("Parcels")[0]
198# Read the local ArcPy feature layer through a feature-class source path.
199from arcgis.features import GeoAccessor, GeoSeriesAccessor
200sdf = pd.DataFrame.spatial.from_featureclass(layer.dataSource)
202# Plot
203sdf.plot(column='VALUE', cmap='YlOrRd', legend=True)
204plt.show()
206# Geocode addresses
207locator = "C:/data/locators/composite.locator"
208results = arcpy.geocoding.GeocodeAddresses(
209 "addresses.csv", locator, "Address Address",
210 "geocoded_results.gdb/addresses"
212```

ArcGIS Pro Notebooks

# ArcGIS Pro Jupyter Notebook
import arcpy
import pandas as pd
import matplotlib.pyplot as plt

# Use current project's map
aprx = arcpy.mp.ArcGISProject("CURRENT")
m = aprx.listMaps()[0]

# Get layer
layer = m.listLayers("Parcels")[0]

# Read the local ArcPy feature layer through a feature-class source path.
from arcgis.features import GeoAccessor, GeoSeriesAccessor
sdf = pd.DataFrame.spatial.from_featureclass(layer.dataSource)

# Plot
sdf.plot(column='VALUE', cmap='YlOrRd', legend=True)
plt.show()

# Geocode addresses
locator = "C:/data/locators/composite.locator"
results = arcpy.geocoding.GeocodeAddresses(
    "addresses.csv", locator, "Address Address",
    "geocoded_results.gdb/addresses"
)
Raw lines 183 to 213
214## GRASS GIS
216### Python API for GRASS
218```python
219import grass.script as gscript
220import grass.script.array as garray
222# Run within an initialized GRASS mapset (for example launch through
223# `grass /path/to/location/mapset --exec python workflow.py`). Merely writing
224# GISDBASE/LOCATION_NAME/MAPSET with g.gisenv does not initialize a session.
226# Import raster
227gscript.run_command('r.in.gdal', input='elevation.tif', output='elevation')
229# Import vector
230gscript.run_command('v.in.ogr', input='roads.shp', output='roads')
232# Get raster info
233info = gscript.raster_info('elevation')
234print(info)
236# Match computational region/resolution to the imported DEM.
237gscript.run_command('g.region', raster='elevation', align='elevation')
238# Slope analysis
239gscript.run_command('r.slope.aspect', elevation='elevation',
240 slope='slope', aspect='aspect')
242# Buffer
243gscript.run_command('v.buffer', input='roads', output='roads_buffer',
244 distance=100)
246# Overlay
247gscript.run_command('v.overlay', ainput='zones', binput='roads',
248 operator='and', output='zones_roads')
250# Calculate statistics
251stats = gscript.parse_command('r.univar', map='elevation', flags='g')
252```

Python API for GRASS

import grass.script as gscript
import grass.script.array as garray

# Run within an initialized GRASS mapset (for example launch through
# `grass /path/to/location/mapset --exec python workflow.py`). Merely writing
# GISDBASE/LOCATION_NAME/MAPSET with g.gisenv does not initialize a session.

# Import raster
gscript.run_command('r.in.gdal', input='elevation.tif', output='elevation')

# Import vector
gscript.run_command('v.in.ogr', input='roads.shp', output='roads')

# Get raster info
info = gscript.raster_info('elevation')
print(info)

# Match computational region/resolution to the imported DEM.
gscript.run_command('g.region', raster='elevation', align='elevation')
# Slope analysis
gscript.run_command('r.slope.aspect', elevation='elevation',
                    slope='slope', aspect='aspect')

# Buffer
gscript.run_command('v.buffer', input='roads', output='roads_buffer',
                    distance=100)

# Overlay
gscript.run_command('v.overlay', ainput='zones', binput='roads',
                    operator='and', output='zones_roads')

# Calculate statistics
stats = gscript.parse_command('r.univar', map='elevation', flags='g')
Raw lines 216 to 253
Runs code python workflow.py :223
254## SAGA GIS
256### Using SAGA via command line
258Tool IDs/parameters belong to a specific SAGA release. For the reviewed 9.12.1
259slope/aspect/curvature tool, the library is `ta_morphometry`, ID `0`:
261```python
262import subprocess
263subprocess.run(['saga_cmd', 'ta_morphometry', '0',
264 '-ELEVATION', 'dem.sgrd', '-SLOPE', 'slope.sgrd',
265 '-ASPECT', 'aspect.sgrd', '-UNIT_SLOPE', '1',
266 '-UNIT_ASPECT', '1'], check=True)
267```
269Use `saga_cmd ta_morphometry 0 -h` and the matching version's tool documentation to
270confirm inputs/output units and method. For grid calculus/channel networks, discover
271the installed library/tool help instead of invented names such as `GridCalculator`
272or `ChannelNetworkAndDrainageBasins`. Parameter interfaces can differ across versions.
273Check exit status and output metadata; process success does not prove scientific validity.

Using SAGA via command line

Tool IDs/parameters belong to a specific SAGA release. For the reviewed 9.12.1 slope/aspect/curvature tool, the library is ta_morphometry, ID 0:

import subprocess
subprocess.run(['saga_cmd', 'ta_morphometry', '0',
                '-ELEVATION', 'dem.sgrd', '-SLOPE', 'slope.sgrd',
                '-ASPECT', 'aspect.sgrd', '-UNIT_SLOPE', '1',
                '-UNIT_ASPECT', '1'], check=True)

Use saga_cmd ta_morphometry 0 -h and the matching version's tool documentation to confirm inputs/output units and method. For grid calculus/channel networks, discover the installed library/tool help instead of invented names such as GridCalculator or ChannelNetworkAndDrainageBasins. Parameter interfaces can differ across versions. Check exit status and output metadata; process success does not prove scientific validity.

Raw lines 256 to 274
275## Cross-Platform Workflows

Cross-Platform Workflows

Raw lines 275 to 276
277### Export QGIS to ArcGIS
279```python
280import geopandas as gpd
282# Read data processed in QGIS
283gdf = gpd.read_file('qgis_output.geojson')
285# Ensure CRS
286gdf = gdf.to_crs('EPSG:32633')
288# Export for ArcGIS as GeoPackage (not File Geodatabase)
289gdf.to_file('arcgis_input.gpkg', driver='GPKG')
290# ArcGIS can read GPKG directly
292# Or export to shapefile
293gdf.to_file('arcgis_input.shp')
294```

Export QGIS to ArcGIS

import geopandas as gpd

# Read data processed in QGIS
gdf = gpd.read_file('qgis_output.geojson')

# Ensure CRS
gdf = gdf.to_crs('EPSG:32633')

# Export for ArcGIS as GeoPackage (not File Geodatabase)
gdf.to_file('arcgis_input.gpkg', driver='GPKG')
# ArcGIS can read GPKG directly

# Or export to shapefile
gdf.to_file('arcgis_input.shp')
Raw lines 277 to 295
296### Batch Processing
298```python
299import geopandas as gpd
300from pathlib import Path
302# Process multiple files
303input_dir = Path('input')
304output_dir = Path('output')
305output_dir.mkdir(parents=True, exist_ok=True)
307for shp in input_dir.glob('*.shp'):
308 gdf = gpd.read_file(shp)
310 if gdf.crs is None:
311 raise ValueError('Source CRS is unknown')
312 gdf = gdf.to_crs(gdf.estimate_utm_crs())
313 # Process in verified metre units
314 gdf['area'] = gdf.geometry.area
315 gdf.geometry = gdf.geometry.buffer(100) # one active geometry for these formats
317 # Export for various platforms
318 basename = shp.stem
319 gdf.to_file(output_dir / f'{basename}_qgis.geojson')
320 gdf.to_file(output_dir / f'{basename}_arcgis.shp')
321```
323For more GIS-specific examples, see [code-examples.md](code-examples.md).
325Sources: [QGIS4.2](https://docs.qgis.org/4.2/en/docs/pyqgis_developer_cookbook/plugins/plugins.html), [ArcGIS viewshed](https://doc.esri.com/en/arcgis-pro/latest/tool-reference/spatial-analyst/viewshed.html), [distance accumulation](https://doc.esri.com/en/arcgis-pro/latest/tool-reference/spatial-analyst/distance-accumulation.html), [GRASS](https://grass.osgeo.org/grass-stable/manuals/r.watershed.html), [SAGA9.12 tool0](https://saga-gis.sourceforge.io/saga_tool_doc/9.12.1/ta_morphometry_0.html).

Batch Processing

import geopandas as gpd
from pathlib import Path

# Process multiple files
input_dir = Path('input')
output_dir = Path('output')
output_dir.mkdir(parents=True, exist_ok=True)

for shp in input_dir.glob('*.shp'):
    gdf = gpd.read_file(shp)

    if gdf.crs is None:
        raise ValueError('Source CRS is unknown')
    gdf = gdf.to_crs(gdf.estimate_utm_crs())
    # Process in verified metre units
    gdf['area'] = gdf.geometry.area
    gdf.geometry = gdf.geometry.buffer(100)  # one active geometry for these formats

    # Export for various platforms
    basename = shp.stem
    gdf.to_file(output_dir / f'{basename}_qgis.geojson')
    gdf.to_file(output_dir / f'{basename}_arcgis.shp')

For more GIS-specific examples, see code-examples.md.

Sources: QGIS4.2, ArcGIS viewshed, distance accumulation, GRASS, SAGA9.12 tool0.

Raw lines 296 to 325