Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add GDALViewshedGenerate binding #142

Merged
merged 34 commits into from
Feb 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
9c97962
Implement `GDALViewshedGenerate` binding
pericles-tpt Jan 22, 2025
f43b9a5
Implement `Viewshed` go function
pericles-tpt Jan 22, 2025
f14b181
Add first Viewshed test (not working yet)
pericles-tpt Jan 22, 2025
6baaabe
Replace `TestViewshedAllVisible` with more comprehensive `TestViewshe…
pericles-tpt Jan 25, 2025
cb15f6d
Add missing documentation comments
pericles-tpt Jan 25, 2025
c41d8a2
Temporarily enable "github workflows" for `viewshed` branch
pericles-tpt Jan 25, 2025
6ec4f2e
Add check to viewshed test to ensure 'scope 2' only runs when GDAL ve…
pericles-tpt Jan 25, 2025
b3facee
Only allow 'dem' height mode in go binding function when GDAL version…
pericles-tpt Jan 25, 2025
8242bc7
Add preprocessor check ensuring `GDALViewshedGenerate` only runs with…
pericles-tpt Jan 25, 2025
ab287ff
Fix incorrect `godalViewshed` function naming
pericles-tpt Jan 25, 2025
b796ab8
Add checks to `Viewshed` for conditions where `godalViewshedGenerate`…
pericles-tpt Jan 25, 2025
b8f67d8
Add error to `Viewshed()` for the "unsupported GDAL version" condition
pericles-tpt Jan 25, 2025
f8d0dd0
Add checks for "invalid GDAL version" errors to `TestViewshedSimpleHe…
pericles-tpt Jan 25, 2025
d3d1718
Fix `godalViewshedGenerate` using unsupported enums for GDAL < 3.1.0
pericles-tpt Jan 25, 2025
2148307
Fix late error for "GDAL version < 3.1.0" condition
pericles-tpt Jan 25, 2025
0c11cfb
Disable "github workflows" for `viewshed` branch
pericles-tpt Jan 25, 2025
862bbeb
Move `forceError()` out of `#if` block
pericles-tpt Jan 28, 2025
cb219cc
Use `ErrorHandler` on all branches of `TestViewshedSimpleHeight`
pericles-tpt Jan 28, 2025
82f2df3
Add `TestViewshedCreationOptions` to test valid/invalid creation options
pericles-tpt Jan 28, 2025
14c242c
Maybe fix 'scanline size' error and add "CreationOptions + ErrLogger"…
pericles-tpt Jan 28, 2025
b6ff5fd
Minor change
pericles-tpt Jan 28, 2025
9f67065
Add missing "min version" checks to `TestViewshedCreationOptions`
pericles-tpt Jan 28, 2025
f67b649
Remove `ErrLogger` from `ViewShed()` call where it's not used
pericles-tpt Jan 28, 2025
5a79c47
Add error for "minimum GDAL version for viewshed" check
pericles-tpt Feb 11, 2025
062d772
Remove GDAL version checks for < 3.4.2
pericles-tpt Feb 11, 2025
c57f4d1
Remove GDAL version checks for < 3.4.2 (in tests)
pericles-tpt Feb 11, 2025
f1b37dd
Replace "min version" checks in `Viewshed()` with a comment documenti…
pericles-tpt Feb 11, 2025
eab0388
Remove assert for error that was removed
pericles-tpt Feb 11, 2025
536a9e6
Add checks for tests that fail when GDAL version < 3.4.2, add back er…
pericles-tpt Feb 12, 2025
223ebd5
Add GDAL version < 3.1.0 check to `TestViewshedCreationOptions`
pericles-tpt Feb 12, 2025
46da29d
Change `Viewshed()` to a method on `Band()` and convert most of its p…
pericles-tpt Feb 12, 2025
5e8164e
Remove "min version 3.1.0" checks
pericles-tpt Feb 12, 2025
f38e1df
Add missing `Close()` calls for datasets in Viewshed tests
pericles-tpt Feb 15, 2025
8f9c622
Add list of available `ViewshedOption`s to `Viewshed` documentation c…
pericles-tpt Feb 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ func (dn DriverName) setRasterizeOpt(to *rasterizeOpts) {
to.driver = dn
}

func (dn DriverName) setViewshedOpt(to *viewshedOpts) {
to.driver = dn
}

type driversOpt struct {
drivers []string
}
Expand Down
4 changes: 4 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ func ErrLogger(fn ErrorHandler) interface {
GridOption
NearblackOption
DemOption
ViewshedOption
SetGCPsOption
GCPsToGeoTransformOption
RegisterPluginOption
Expand Down Expand Up @@ -388,6 +389,9 @@ func (ec errorCallback) setNearblackOpt(o *nearBlackOpts) {
func (ec errorCallback) setDemOpt(o *demOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setViewshedOpt(o *viewshedOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setSetGCPsOpt(o *setGCPsOpts) {
o.errorHandler = ec.fn
}
Expand Down
18 changes: 18 additions & 0 deletions godal.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1905,6 +1905,24 @@ GDALDatasetH godalDem(cctx *ctx, const char *pszDest, const char *pszProcessing,
return ret;
}

GDALDatasetH godalViewshedGenerate(cctx *ctx, GDALRasterBandH bnd, const char *pszDriverName, const char *pszTargetRasterName, const char **papszCreationOptions, double dfObserverX,
double dfObserverY, double dfObserverHeight, double dfTargetHeight, double dfVisibleVal, double dfInvisibleVal, double dfOutOfRangeVal,
double dfNoDataVal, double dfCurvCoeff, GUInt32 eMode, double dfMaxDistance, GUInt32 heightMode) {
godalWrap(ctx);
GDALDatasetH ret = nullptr;
#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3, 1, 0)
ret = GDALViewshedGenerate(bnd, pszDriverName, pszTargetRasterName, papszCreationOptions, dfObserverX, dfObserverY, dfObserverHeight, dfTargetHeight, dfVisibleVal, dfInvisibleVal, dfOutOfRangeVal,
dfNoDataVal, dfCurvCoeff, GDALViewshedMode(eMode), dfMaxDistance, nullptr, nullptr, GDALViewshedOutputType(heightMode), nullptr);
#else
CPLError(CE_Failure, CPLE_AppDefined, "Viewshed not implemented in gdal < 3.1");
#endif
if(ret == nullptr) {
forceError(ctx);
}
godalUnwrap();
return ret;
}

OGRSpatialReferenceH godalGetGCPSpatialRef(GDALDatasetH hSrcDS) {
return GDALGetGCPSpatialRef(hSrcDS);
}
Expand Down
90 changes: 90 additions & 0 deletions godal.go
Original file line number Diff line number Diff line change
Expand Up @@ -4060,6 +4060,96 @@ func (ds *Dataset) Dem(destPath, processingMode string, colorFilename string, sw
return &Dataset{majorObject{C.GDALMajorObjectH(dsRet)}}, nil
}

// ViewshedMode is the "cell height calculation mode" for the viewshed process
//
// Source: https://github.com/OSGeo/gdal/blob/master/alg/viewshed/viewshed_types.h
type ViewshedMode uint32

const (
// MDiagonal is the "diagonal mode"
MDiagonal ViewshedMode = iota + 1
// MEdge is the "edge mode"
MEdge
// MMax is the "maximum value produced by Diagonal and Edge mode"
MMax
// MMin is the "minimum value produced by Diagonal and Edge mode"
MMin
)

// ViewshedOutputType sets the return type and information represented by the returned data
//
// Source: https://gdal.org/en/stable/programs/gdal_viewshed.html
//
// NOTE: "Cumulative (ACCUM)" mode not currently supported, as it's not available in the `GDALViewshedGenerate` function
// (it's only used in the command line invocation of `viewshed`)
type ViewshedOutputType uint32

const (
// Normal returns a raster of type Byte containing visible locations
Normal ViewshedOutputType = iota + 1
// MinTargetHeightFromDem return a raster of type Float64 containing the minimum target height for target to be visible from the DEM surface
MinTargetHeightFromDem
// MinTargetHeightFromGround return a raster of type Float64 containing the minimum target height for target to be visible from ground level
MinTargetHeightFromGround
)

// Viewshed (binding for GDALViewshedGenerate), creates a viewshed from a raster DEM, these parameters (mostly) map to parameters for GDALViewshedGenerate
// for more information see: https://gdal.org/en/stable/api/gdal_alg.html#_CPPv420GDALViewshedGenerate15GDALRasterBandHPKcPKc12CSLConstListddddddddd16GDALViewshedModed16GDALProgressFuncPv22GDALViewshedOutputType12CSLConstList
//
// Several Viewshed parameters have default values defined in GDAL, see `Options` in: https://github.com/OSGeo/gdal/blob/master/alg/viewshed/viewshed_types.h
//
// to define different values for these parameters, provide the corresponding options:
//
// - DriverName(dn DriverName) sets the GDAL driver
// - TargetHeight(h float64) sets the target height above the DEM surface
// - VisibilityVals(vv float64, iv float64) sets the raster output value for visible and invisible pixels
// - OutOfRangeVal(ov float64) sets the raster output value for pixels outside of max distance
// - MaxDistance(d float64) sets the maximum number of pixels to search from the observer
// - CurveCoeff(cc float64) sets the coefficient for atmospheric refraction
// - NoDataVal(nd float64) sets the raster output value for pixels with no data
// - CellMode(cm ViewshedMode) sets mode of cell height calculation
// - HeightMode(hm ViewshedOutputType) sets the type of the output information
// - CreationOption(opts ...string) options to pass to a driver when creating a dataset
//
// WARNING: One of the Godal tests for this function is ported from a GDAL test finalised in this commit (tagged for 3.10.2RC1): https://github.com/OSGeo/gdal/commit/33dd00c63155250afce04092c77cb225570efa64
//
// Automated testing across different GDAL versions shows that GDALViewshedGenerate FAILS some of these tests on versions of GDAL < 3.10.0 as follows:
//
// - version < 3.1.0: viewshed is not supported and will always throw a "not implemented" error
// - 3.1.0 <= version < 3.4.2: all tests fail
// - 3.4.2 <= version < 3.10.0: tests where heightMode == MinTargetHeightFromDem fail
// - version >= 3.10.0: all tests pass
func (srcBand Band) Viewshed(targetRasterName string, observerX float64, observerY float64, observerHeight float64, opts ...ViewshedOption) (*Dataset, error) {
vso := viewshedOpts{
driver: GTiff,
visibleVal: 255,
noDataVal: -1,
curveCoeff: .85714,
cellMode: MEdge,
heightMode: Normal,
}
for _, opt := range opts {
opt.setViewshedOpt(&vso)
}

copts := sliceToCStringArray(vso.creation)
defer copts.free()
driver := unsafe.Pointer(C.CString(string(vso.driver)))
defer C.free(unsafe.Pointer(driver))
targetRaster := unsafe.Pointer(C.CString(targetRasterName))
defer C.free(unsafe.Pointer(targetRaster))

cgc := createCGOContext(nil, vso.errorHandler)
dsRet := C.godalViewshedGenerate(cgc.cPointer(), srcBand.handle(), (*C.char)(driver), (*C.char)(targetRaster), copts.cPointer(), C.double(observerX),
C.double(observerY), C.double(observerHeight), C.double(vso.targetHeight), C.double(vso.visibleVal), C.double(vso.invisibleVal), C.double(vso.outOfRangeVal),
C.double(vso.noDataVal), C.double(vso.curveCoeff), C.uint(vso.cellMode), C.double(vso.maxDistance), C.uint(vso.heightMode))
if err := cgc.close(); err != nil {
return nil, err
}

return &Dataset{majorObject{C.GDALMajorObjectH(dsRet)}}, nil
}

// Nearblack runs the library version of nearblack
//
// See the nearblack doc page to determine the valid flags/opts that can be set in switches.
Expand Down
4 changes: 4 additions & 0 deletions godal.h
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,10 @@ extern "C" {
GDALDatasetH godalGrid(cctx *ctx, const char *pszDest, GDALDatasetH hSrcDS, char **switches);
GDALDatasetH godalNearblack(cctx *ctx, const char *pszDest, GDALDatasetH hDstDS, GDALDatasetH hSrcDS, char **switches);
GDALDatasetH godalDem(cctx *ctx, const char *pszDest, const char *pszProcessing, const char *pszColorFilename, GDALDatasetH hSrcDS, char **switches);
GDALDatasetH godalViewshedGenerate(cctx *ctx, GDALRasterBandH bnd, const char *pszDriverName, const char *pszTargetRasterName, const char **papszCreationOptions,
double dfObserverX, double dfObserverY, double dfObserverHeight, double dfTargetHeight, double dfVisibleVal,
double dfInvisibleVal, double dfOutOfRangeVal, double dfNoDataVal, double dfCurvCoeff, GUInt32 eMode,
double dfMaxDistance, GUInt32 heightMode);

typedef struct {
const GDAL_GCP *gcpList;
Expand Down
164 changes: 164 additions & 0 deletions godal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3851,7 +3851,7 @@

func (dl *debugLogger) L(ec ErrorCategory, code int, msg string) error {
if ec >= CE_Warning {
return fmt.Errorf(msg)

Check failure on line 3854 in godal_test.go

View workflow job for this annotation

GitHub Actions / Go 1.23 + GDAL release/3.10 test

SA1006: printf-style function with dynamic format string and no further arguments should use print-style function instead (staticcheck)
}
if ec == CE_Debug {
dl.logs += ",GOTESTDEBUG:" + msg
Expand Down Expand Up @@ -4440,6 +4440,170 @@
assert.Error(t, err)
}

// Test Ported from: https://github.com/OSGeo/gdal/blob/6cdae8b8f7d09ecf67e24959e984d2e7bbe3ee62/autotest/cpp/test_viewshed.cpp#L98
func TestViewshedSimpleHeight(t *testing.T) {
// setup common to all scopes
var (
ehc = eh()
drv = DriverName("MEM")

identity = [6]float64{0, 1, 0, 0, 0, 1}

xLen = 5
yLen = 5
in = []int8{
-1, 0, 1, 0, -1,
-1, 2, 0, 4, -1,
-1, 1, 0, -1, -1,
0, 3, 0, 2, 0,
-1, 0, 0, 3, -1,
}
observable = []float64{
4, 2, 0, 4, 8,
3, 2, 0, 4, 3,
2, 1, 0, -1, -2,
4, 3, 0, 2, 1,
6, 3, 0, 2, 4,
}
)
vrtDs, err := Create(Memory, "", 1, Int8, xLen, yLen)
if err != nil {
t.Error(err)
return
}
defer vrtDs.Close()
err = vrtDs.SetGeoTransform(identity)
if err != nil {
t.Error(err)
return
}
err = vrtDs.Bands()[0].IO(IOWrite, 0, 0, in, xLen, yLen)
if err != nil {
t.Error(err)
return
}

// from cpp scope 1: normal
// NOTE: This test fails in releases older than 3.4.2
if CheckMinVersion(3, 4, 2) {
rds, err := vrtDs.Bands()[0].Viewshed("none", 2, 2, 0, DriverName(drv), CurveCoeff(0), ErrLogger(ehc.ErrorHandler))
if err != nil {
t.Error(err)
return
}
defer rds.Close()

out := make([]int8, xLen*yLen)
err = rds.Bands()[0].IO(IORead, 0, 0, out, xLen, yLen)
if err != nil {
t.Error(err)
return
}

expected := make([]int8, xLen*yLen)
for i := 0; i < len(in); i++ {
var v int8 = 0
if in[i] >= int8(observable[i]) {
v = 127
}
expected[i] = v
}
assert.Equal(t, expected, out)
}

// from cpp scope 2: dem
// NOTE: This test fails in releases older than 3.10
if CheckMinVersion(3, 10, 0) {
rds, err := vrtDs.Bands()[0].Viewshed("none", 2, 2, 0, DriverName(drv), CurveCoeff(0), HeightMode(MinTargetHeightFromDem))
if err != nil {
t.Error(err)
return
}
defer rds.Close()

dem := make([]float64, xLen*yLen)
err = rds.Bands()[0].IO(IORead, 0, 0, dem, xLen, yLen)
if err != nil {
t.Error(err)
return
}

expected := make([]float64, xLen*yLen)
copy(expected, observable)
for i := 0; i < len(expected); i++ {
expected[i] = math.Max(0.0, expected[i])
}
assert.Equal(t, expected, dem)
}

// from cpp scope 3: ground
// NOTE: This test fails in releases older than 3.4.2
if CheckMinVersion(3, 4, 2) {
rds, err := vrtDs.Bands()[0].Viewshed("none", 2, 2, 0, DriverName(drv), CurveCoeff(0), HeightMode(MinTargetHeightFromGround))
if err != nil {
t.Error(err)
return
}
defer rds.Close()

ground := make([]float64, xLen*yLen)
err = rds.Bands()[0].IO(IORead, 0, 0, ground, xLen, yLen)
if err != nil {
t.Error(err)
return
}

expected := make([]float64, xLen*yLen)
for i := 0; i < len(expected); i++ {
expected[i] = math.Max(0.0, observable[i]-float64(in[i]))
}
assert.Equal(t, expected, ground)
}
}

func TestViewshedCreationOptions(t *testing.T) {

var (
driver = GTiff
tmpname = tempfile()
)
defer os.Remove(tmpname)
vrtDs, err := Create(driver, tmpname, 1, Int8, 20, 20, CreationOption("TILED=YES", "BLOCKXSIZE=128", "BLOCKYSIZE=128"))
if err != nil {
t.Error(err)
return
}
defer vrtDs.Close()
identity := [6]float64{0, 1, 0, 0, 0, 1}
err = vrtDs.SetGeoTransform(identity)
if err != nil {
t.Error(err)
return
}

// Invalid - with error logger
ehc := eh()
ds, err := vrtDs.Bands()[0].Viewshed("none", 2, 2, 0, CreationOption("INVALID_OPT=BAR"), ErrLogger(ehc.ErrorHandler))
if err == nil {
ds.Close()
}
assert.Error(t, err)

// Invalid - no error logger
ds, err = vrtDs.Bands()[0].Viewshed("none", 2, 2, 0, CreationOption("INVALID_OPT=BAR"))
if err == nil {
ds.Close()
}
assert.Error(t, err)

// Valid
ds, err = vrtDs.Bands()[0].Viewshed("none", 2, 2, 0, CreationOption("TILED=YES", "BLOCKXSIZE=128", "BLOCKYSIZE=128"))
if err == nil {
ds.Close()
}
assert.NoError(t, err)
}

func TestNearblackBlack(t *testing.T) {
// 1. Create an image, linearly interpolated, from black (on the left) to white (on the right), using `Grid()`
var (
Expand Down Expand Up @@ -4489,7 +4653,7 @@
defer func() { _ = VSIUnlink(fname) }()
defer gridDs.Close()
originalColors := make([]byte, outXSize*outYSize)
gridDs.Read(0, 0, originalColors, outXSize, outYSize)

Check failure on line 4656 in godal_test.go

View workflow job for this annotation

GitHub Actions / Go 1.23 + GDAL release/3.10 test

Error return value of `gridDs.Read` is not checked (errcheck)

// 2. Put the Dataset generated above, through the `Nearblack` function, to set pixels near BLACK to BLACK
argsNbString := "-near 10 -nb 0"
Expand All @@ -4502,7 +4666,7 @@
defer func() { _ = VSIUnlink(fname2) }()
defer nbDs.Close()
nearblackColors := make([]byte, outXSize*outYSize)
nbDs.Read(0, 0, nearblackColors, outXSize, outYSize)

Check failure on line 4669 in godal_test.go

View workflow job for this annotation

GitHub Actions / Go 1.23 + GDAL release/3.10 test

Error return value of `nbDs.Read` is not checked (errcheck)

// 3. Test on all rows that pixels where abs(0 - pixelValue) <= 10, are set to black (0)
for i := 0; i < outYSize; i++ {
Expand Down Expand Up @@ -4561,7 +4725,7 @@
defer func() { _ = VSIUnlink(fname) }()
defer gridDs.Close()
originalColors := make([]byte, outXSize*outYSize)
gridDs.Read(0, 0, originalColors, outXSize, outYSize)

Check failure on line 4728 in godal_test.go

View workflow job for this annotation

GitHub Actions / Go 1.23 + GDAL release/3.10 test

Error return value of `gridDs.Read` is not checked (errcheck)

// 2. Put the Dataset generated above, through the `Nearblack` function, to set pixels near WHITE to WHITE
argsNbString := "-near 10 -nb 0 -white"
Expand All @@ -4574,7 +4738,7 @@
defer func() { _ = VSIUnlink(fname2) }()
defer nbDs.Close()
nearblackColors := make([]byte, outXSize*outYSize)
nbDs.Read(0, 0, nearblackColors, outXSize, outYSize)

Check failure on line 4741 in godal_test.go

View workflow job for this annotation

GitHub Actions / Go 1.23 + GDAL release/3.10 test

Error return value of `nbDs.Read` is not checked (errcheck)

// 3. Test on all rows that pixels where abs(255 - pixelValue) <= 10, are set to white (255)
for i := 0; i < outYSize; i++ {
Expand Down Expand Up @@ -4660,7 +4824,7 @@
defer func() { _ = VSIUnlink(fname) }()
defer gridDs.Close()
originalColors := make([]byte, outXSize*outYSize)
gridDs.Read(0, 0, originalColors, outXSize, outYSize)

Check failure on line 4827 in godal_test.go

View workflow job for this annotation

GitHub Actions / Go 1.23 + GDAL release/3.10 test

Error return value of `gridDs.Read` is not checked (errcheck)

// 2. Put the Dataset generated above, through the `Nearblack` function, to set pixels near BLACK to BLACK
nbDs, err := Create(Memory, "nbDs", 1, Byte, outXSize, outYSize)
Expand All @@ -4676,7 +4840,7 @@
return
}
nearblackColors := make([]byte, outXSize*outYSize)
nbDs.Read(0, 0, nearblackColors, outXSize, outYSize)

Check failure on line 4843 in godal_test.go

View workflow job for this annotation

GitHub Actions / Go 1.23 + GDAL release/3.10 test

Error return value of `nbDs.Read` is not checked (errcheck)

// 3. Test on all rows that pixels where abs(0 - pixelValue) <= 10, are set to black (0)
for i := 0; i < outYSize; i++ {
Expand Down Expand Up @@ -5088,7 +5252,7 @@
return
}
}
gridDs.SetProjection("epsg:32632")

Check failure on line 5255 in godal_test.go

View workflow job for this annotation

GitHub Actions / Go 1.23 + GDAL release/3.10 test

Error return value of `gridDs.SetProjection` is not checked (errcheck)
defer func() { _ = VSIUnlink(fname) }()
defer gridDs.Close()

Expand Down Expand Up @@ -5223,7 +5387,7 @@
return
}
}
gridDs.SetProjection("epsg:32632")

Check failure on line 5390 in godal_test.go

View workflow job for this annotation

GitHub Actions / Go 1.23 + GDAL release/3.10 test

Error return value of `gridDs.SetProjection` is not checked (errcheck)
defer func() { _ = VSIUnlink(fname) }()
defer gridDs.Close()

Expand Down
Loading
Loading