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

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 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
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 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
16 changes: 16 additions & 0 deletions godal.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1905,6 +1905,22 @@ 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);
#endif
if(ret == nullptr) {
forceError(ctx);
}
godalUnwrap();
return ret;
}

OGRSpatialReferenceH godalGetGCPSpatialRef(GDALDatasetH hSrcDS) {
return GDALGetGCPSpatialRef(hSrcDS);
}
Expand Down
82 changes: 82 additions & 0 deletions godal.go
Original file line number Diff line number Diff line change
Expand Up @@ -4060,6 +4060,88 @@ 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 to parameters for GDALViewshedGenerate
//
// for more information see: https://gdal.org/en/stable/api/gdal_alg.html#_CPPv420GDALViewshedGenerate15GDALRasterBandHPKcPKc12CSLConstListddddddddd16GDALViewshedModed16GDALProgressFuncPv22GDALViewshedOutputType12CSLConstList
//
// Creations options can be set through options with:
//
// Viewshed(bnd, "mem", "none", ..., CreationOption("TILED=YES","BLOCKXSIZE=256"))
func Viewshed(targetBand Band, driverName *DriverName, targetRasterName string, observerX float64, observerY float64, observerHeight float64, targetHeight float64,
visibleVal float64, invisibleVal float64, outOfRangeVal float64, noDataVal float64, curveCoeff float64, mode ViewshedMode, maxDistance float64,
heightMode ViewshedOutputType, opts ...ViewshedOption) (*Dataset, error) {

// TODO: Should I put a 'warning' in the documentation for `Viewshed` instead of disallowing the last two configurations?
if !CheckMinVersion(3, 1, 0) {
return nil, errors.New("failed to run, 'viewshed' not supported on GDAL versions < 3.1.0")
} else if !CheckMinVersion(3, 4, 2) {
return nil, errors.New("cannot run 'viewshed' with GDAL version <= 3.4.1, as some tests produce invalid results under these conditions")
} else if !CheckMinVersion(3, 10, 0) && heightMode == MinTargetHeightFromDem {
return nil, errors.New("height mode CANNOT be `MinTargetHeightFromDem` when running a GDAL version < 3.10, as some tests produce invalid results under these conditions")
}

// Allow `driverName` to be null and handle it here to match parameter/behaviour of GDALViewshedGenerate
defaultDriverName := GTiff
if driverName == nil {
driverName = &defaultDriverName
}

viewshedOpts := viewshedOpts{}
for _, opt := range opts {
opt.setViewshedOpt(&viewshedOpts)
}

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

cgc := createCGOContext(nil, viewshedOpts.errorHandler)
dsRet := C.godalViewshedGenerate(cgc.cPointer(), targetBand.handle(), (*C.char)(driver), (*C.char)(targetRaster), copts.cPointer(), C.double(observerX),
C.double(observerY), C.double(observerHeight), C.double(targetHeight), C.double(visibleVal), C.double(invisibleVal), C.double(outOfRangeVal),
C.double(noDataVal), C.double(curveCoeff), C.uint(mode), C.double(maxDistance), C.uint(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
173 changes: 173 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

printf: non-constant format string in call to fmt.Errorf (govet)
}
if ec == CE_Debug {
dl.logs += ",GOTESTDEBUG:" + msg
Expand Down Expand Up @@ -4440,6 +4440,179 @@
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) {
if !CheckMinVersion(3, 1, 0) {
_, err := Viewshed(Band{}, nil, "none", 1, 1, 0, 0, 255, 0, 0, -1, 0, MEdge, 0, Normal)
assert.EqualError(t, err, "failed to run, 'viewshed' not supported on GDAL versions < 3.1.0")
return
} else if !CheckMinVersion(3, 4, 2) {
_, err := Viewshed(Band{}, nil, "none", 1, 1, 0, 0, 255, 0, 0, -1, 0, MEdge, 0, Normal)
assert.EqualError(t, err, "cannot run 'viewshed' with GDAL version <= 3.4.1, as some tests produce invalid results under these conditions")
return
}

// setup common to all scopes
var (
ehc = eh()
driver = 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
}
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
{
rds, err := Viewshed(vrtDs.Bands()[0], &driver, "none", 2, 2, 0, 0, 255, 0, 0, -1, 0, MEdge, 0, Normal, 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 := Viewshed(vrtDs.Bands()[0], &driver, "none", 2, 2, 0, 0, 255, 0, 0, -1, 0, MEdge, 0, 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)
} else {
_, err := Viewshed(vrtDs.Bands()[0], &driver, "none", 2, 2, 0, 0, 255, 0, 0, -1, 0, MEdge, 0, MinTargetHeightFromDem)
assert.EqualError(t, err, "height mode CANNOT be `MinTargetHeightFromDem` when running a GDAL version < 3.10, as some tests produce invalid results under these conditions")
}

// from cpp scope 3: ground
{
rds, err := Viewshed(vrtDs.Bands()[0], &driver, "none", 2, 2, 0, 0, 255, 0, 0, -1, 0, MEdge, 0, 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) {
if !CheckMinVersion(3, 1, 0) {
_, err := Viewshed(Band{}, nil, "none", 1, 1, 0, 0, 255, 0, 0, -1, 0, MEdge, 0, Normal)
assert.EqualError(t, err, "failed to run, 'viewshed' not supported on GDAL versions < 3.1.0")
return
} else if !CheckMinVersion(3, 4, 2) {
_, err := Viewshed(Band{}, nil, "none", 1, 1, 0, 0, 255, 0, 0, -1, 0, MEdge, 0, Normal)
assert.EqualError(t, err, "cannot run 'viewshed' with GDAL version <= 3.4.1, as some tests produce invalid results under these conditions")
return
}

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
}
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()
_, err = Viewshed(vrtDs.Bands()[0], &driver, "none", 2, 2, 0, 0, 255, 0, 0, -1, 0, MEdge, 0, Normal, CreationOption("INVALID_OPT=BAR"), ErrLogger(ehc.ErrorHandler))
assert.Error(t, err)

// Invalid - no error logger
_, err = Viewshed(vrtDs.Bands()[0], &driver, "none", 2, 2, 0, 0, 255, 0, 0, -1, 0, MEdge, 0, Normal, CreationOption("INVALID_OPT=BAR"))
assert.Error(t, err)

// Valid
_, err = Viewshed(vrtDs.Bands()[0], &driver, "none", 2, 2, 0, 0, 255, 0, 0, -1, 0, MEdge, 0, Normal, CreationOption("TILED=YES", "BLOCKXSIZE=128", "BLOCKYSIZE=128"))
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 +4662,7 @@
defer func() { _ = VSIUnlink(fname) }()
defer gridDs.Close()
originalColors := make([]byte, outXSize*outYSize)
gridDs.Read(0, 0, originalColors, outXSize, outYSize)

Check failure on line 4665 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 +4675,7 @@
defer func() { _ = VSIUnlink(fname2) }()
defer nbDs.Close()
nearblackColors := make([]byte, outXSize*outYSize)
nbDs.Read(0, 0, nearblackColors, outXSize, outYSize)

Check failure on line 4678 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 +4734,7 @@
defer func() { _ = VSIUnlink(fname) }()
defer gridDs.Close()
originalColors := make([]byte, outXSize*outYSize)
gridDs.Read(0, 0, originalColors, outXSize, outYSize)

Check failure on line 4737 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 +4747,7 @@
defer func() { _ = VSIUnlink(fname2) }()
defer nbDs.Close()
nearblackColors := make([]byte, outXSize*outYSize)
nbDs.Read(0, 0, nearblackColors, outXSize, outYSize)

Check failure on line 4750 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 +4833,7 @@
defer func() { _ = VSIUnlink(fname) }()
defer gridDs.Close()
originalColors := make([]byte, outXSize*outYSize)
gridDs.Read(0, 0, originalColors, outXSize, outYSize)

Check failure on line 4836 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 +4849,7 @@
return
}
nearblackColors := make([]byte, outXSize*outYSize)
nbDs.Read(0, 0, nearblackColors, outXSize, outYSize)

Check failure on line 4852 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 +5261,7 @@
return
}
}
gridDs.SetProjection("epsg:32632")

Check failure on line 5264 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 +5396,7 @@
return
}
}
gridDs.SetProjection("epsg:32632")

Check failure on line 5399 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
14 changes: 14 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -894,6 +894,7 @@ func CreationOption(opts ...string) interface {
DatasetVectorTranslateOption
GMLExportOption
RasterizeOption
ViewshedOption
} {
return creationOpt{opts}
}
Expand All @@ -916,6 +917,9 @@ func (co creationOpt) setGMLExportOpt(gmlo *gmlExportOpts) {
func (co creationOpt) setRasterizeOpt(o *rasterizeOpts) {
o.create = append(o.create, co.creation...)
}
func (co creationOpt) setViewshedOpt(dc *viewshedOpts) {
dc.creation = append(dc.creation, co.creation...)
}

type configOpt struct {
config []string
Expand Down Expand Up @@ -1276,6 +1280,16 @@ type DemOption interface {
setDemOpt(demOpt *demOpts)
}

type viewshedOpts struct {
creation []string
errorHandler ErrorHandler
}

// ViewshedOption is an option that can be passed to Viewshed()
type ViewshedOption interface {
setViewshedOpt(viewshedOpt *viewshedOpts)
}

type setGCPsOpts struct {
errorHandler ErrorHandler
projString string
Expand Down
Loading