#!/usr/bin/env pytest
# -*- coding: utf-8 -*-
###############################################################################
#
# Project:  GDAL/OGR Test Suite
# Purpose:  Test the GenImgProjTransformer capabilities.
# Author:   Frank Warmerdam <warmerdam@pobox.com>
#
###############################################################################
# Copyright (c) 2008, Frank Warmerdam <warmerdam@pobox.com>
# Copyright (c) 2008-2013, Even Rouault <even dot rouault at spatialys.com>
#
# SPDX-License-Identifier: MIT
###############################################################################


import math

import gdaltest
import pytest

from osgeo import gdal, osr

###############################################################################
# Test simple Geotransform based transformer.


def test_transformer_1():

    ds = gdal.Open("data/byte.tif")
    tr = gdal.Transformer(ds, None, [])

    success, pnt = tr.TransformPoint(0, 20, 10)

    assert (
        success
        and pnt[0] == pytest.approx(441920, abs=0.00000001)
        and pnt[1] == pytest.approx(3750720, abs=0.00000001)
        and pnt[2] == 0.0
    ), "got wrong forward transform result."

    success, pnt = tr.TransformPoint(1, pnt[0], pnt[1], pnt[2])

    assert (
        success
        and pnt[0] == pytest.approx(20, abs=0.00000001)
        and pnt[1] == pytest.approx(10, abs=0.00000001)
        and pnt[2] == 0.0
    ), "got wrong reverse transform result."


###############################################################################
# Test GCP based transformer with polynomials.


@pytest.mark.skipif(
    not gdaltest.vrt_has_open_support(),
    reason="VRT driver open missing",
)
def test_transformer_2():

    ds = gdal.Open("data/gcps.vrt")
    tr = gdal.Transformer(ds, None, ["METHOD=GCP_POLYNOMIAL"])

    success, pnt = tr.TransformPoint(0, 20, 10)

    assert (
        success
        and pnt[0] == pytest.approx(441920, abs=0.001)
        and pnt[1] == pytest.approx(3750720, abs=0.001)
        and pnt[2] == 0
    ), "got wrong forward transform result."

    success, pnt = tr.TransformPoint(1, pnt[0], pnt[1], pnt[2])

    assert (
        success
        and pnt[0] == pytest.approx(20, abs=0.001)
        and pnt[1] == pytest.approx(10, abs=0.001)
        and pnt[2] == 0
    ), "got wrong reverse transform result."


###############################################################################
# Test GCP based transformer with thin plate splines.


@pytest.mark.skipif(
    not gdaltest.vrt_has_open_support(),
    reason="VRT driver open missing",
)
def test_transformer_3():

    ds = gdal.Open("data/gcps.vrt")
    tr = gdal.Transformer(ds, None, ["METHOD=GCP_TPS"])

    success, pnt = tr.TransformPoint(0, 20, 10)

    assert (
        success
        and pnt[0] == pytest.approx(441920, abs=0.001)
        and pnt[1] == pytest.approx(3750720, abs=0.001)
        and pnt[2] == 0
    ), "got wrong forward transform result."

    success, pnt = tr.TransformPoint(1, pnt[0], pnt[1], pnt[2])

    assert (
        success
        and pnt[0] == pytest.approx(20, abs=0.001)
        and pnt[1] == pytest.approx(10, abs=0.001)
        and pnt[2] == 0
    ), "got wrong reverse transform result."


###############################################################################
# Test GCP based transformer with homography.


@pytest.mark.skipif(
    not gdaltest.vrt_has_open_support(),
    reason="VRT driver open missing",
)
def test_transformer_homography():

    ds = gdal.Open("data/gcps.vrt")
    tr = gdal.Transformer(ds, None, ["METHOD=GCP_HOMOGRAPHY"])

    success, pnt = tr.TransformPoint(0, 20, 10)

    assert (
        success
        and pnt[0] == pytest.approx(441920, abs=0.001)
        and pnt[1] == pytest.approx(3750720, abs=0.001)
        and pnt[2] == 0
    ), "got wrong forward transform result."

    success, pnt = tr.TransformPoint(1, pnt[0], pnt[1], pnt[2])

    assert (
        success
        and pnt[0] == pytest.approx(20, abs=0.001)
        and pnt[1] == pytest.approx(10, abs=0.001)
        and pnt[2] == 0
    ), "got wrong reverse transform result."


###############################################################################
# Test geolocation based transformer.


@pytest.mark.skipif(
    not gdaltest.vrt_has_open_support(),
    reason="VRT driver open missing",
)
def test_transformer_4():

    ds = gdal.Open("data/sstgeo.vrt")
    tr = gdal.Transformer(ds, None, ["METHOD=GEOLOC_ARRAY"])

    success, pnt = tr.TransformPoint(0, 20, 10)

    assert (
        success
        and pnt[0] == pytest.approx(-81.961341857910156, abs=0.000001)
        and pnt[1] == pytest.approx(29.612689971923828, abs=0.000001)
        and pnt[2] == 0
    ), "got wrong forward transform result."

    success, pnt = tr.TransformPoint(1, pnt[0], pnt[1], pnt[2])

    assert (
        success
        and pnt[0] == pytest.approx(20, abs=0.000001)
        and pnt[1] == pytest.approx(10, abs=0.000001)
        and pnt[2] == 0
    ), "got wrong reverse transform result."


###############################################################################
# Test RPC based transformer.


@pytest.mark.skipif(
    not gdaltest.vrt_has_open_support(),
    reason="VRT driver open missing",
)
def test_transformer_5():

    ds = gdal.Open("data/rpc.vrt")
    tr = gdal.Transformer(ds, None, ["METHOD=RPC", "RPC_PIXEL_ERROR_THRESHOLD=0.05"])

    success, pnt = tr.TransformPoint(0, 20.5, 10.5)
    assert (
        success
        and pnt[0] == pytest.approx(125.64830100509131, abs=0.000001)
        and pnt[1] == pytest.approx(39.869433991997553, abs=0.000001)
        and pnt[2] == 0
    ), "got wrong forward transform result."

    success, pnt = tr.TransformPoint(1, pnt[0], pnt[1], pnt[2])
    assert (
        success
        and pnt[0] == pytest.approx(20.5, abs=0.05)
        and pnt[1] == pytest.approx(10.5, abs=0.05)
        and pnt[2] == 0
    ), "got wrong reverse transform result."

    # Try with a different height.

    success, pnt = tr.TransformPoint(0, 20.5, 10.5, 30)

    assert (
        success
        and pnt[0] == pytest.approx(125.64828521533849, abs=0.000001)
        and pnt[1] == pytest.approx(39.869345204440144, abs=0.000001)
        and pnt[2] == 30
    ), "got wrong forward transform result.(2)"

    success, pnt = tr.TransformPoint(1, pnt[0], pnt[1], pnt[2])

    assert (
        success
        and pnt[0] == pytest.approx(20.5, abs=0.05)
        and pnt[1] == pytest.approx(10.5, abs=0.05)
        and pnt[2] == 30
    ), "got wrong reverse transform result.(2)"

    # Test RPC_HEIGHT option
    tr = gdal.Transformer(ds, None, ["METHOD=RPC", "RPC_HEIGHT=30"])

    success, pnt = tr.TransformPoint(0, 20.5, 10.5)

    assert (
        success
        and pnt[0] == pytest.approx(125.64828521533849, abs=0.000001)
        and pnt[1] == pytest.approx(39.869345204440144, abs=0.000001)
    ), "got wrong forward transform result.(3)"

    success, pnt = tr.TransformPoint(1, pnt[0], pnt[1], pnt[2])

    assert (
        success
        and pnt[0] == pytest.approx(20.5, abs=0.1)
        and pnt[1] == pytest.approx(10.5, abs=0.1)
    ), "got wrong reverse transform result.(3)"

    # Test RPC_DEM and RPC_HEIGHT_SCALE options

    # (long,lat)=(125.64828521533849 39.869345204440144) -> (Easting,Northing)=(213324.662167036 4418634.47813677) in EPSG:32652
    ds_dem = gdal.GetDriverByName("GTiff").Create("/vsimem/dem.tif", 100, 100, 1)
    sr = osr.SpatialReference()
    sr.ImportFromEPSG(32652)
    ds_dem.SetProjection(sr.ExportToWkt())
    ds_dem.SetGeoTransform([213300, 200, 0, 4418700, 0, -200])
    ds_dem.GetRasterBand(1).Fill(15)
    ds_dem = None

    tr = gdal.Transformer(
        ds, None, ["METHOD=RPC", "RPC_HEIGHT_SCALE=2", "RPC_DEM=/vsimem/dem.tif"]
    )

    success, pnt = tr.TransformPoint(0, 20.5, 10.5, 0)

    assert (
        success
        and pnt[0] == pytest.approx(125.64828521533849, abs=0.000001)
        and pnt[1] == pytest.approx(39.869345204440144, abs=0.000001)
    ), "got wrong forward transform result.(4)"

    success, pnt = tr.TransformPoint(1, pnt[0], pnt[1], pnt[2])

    assert (
        success
        and pnt[0] == pytest.approx(20.5, abs=0.05)
        and pnt[1] == pytest.approx(10.5, abs=0.05)
    ), "got wrong reverse transform result.(4)"

    tr = None

    # Test HEIGHT_DEFAULT in RPC metadata domain
    tmp_ds = gdal.GetDriverByName("VRT").CreateCopy("", ds)
    tmp_ds.SetMetadataItem("HEIGHT_DEFAULT", "30", "RPC")
    tr = gdal.Transformer(tmp_ds, None, ["METHOD=RPC"])

    success, pnt = tr.TransformPoint(0, 20.5, 10.5)

    assert (
        success
        and pnt[0] == pytest.approx(125.64828521533849, abs=0.000001)
        and pnt[1] == pytest.approx(39.869345204440144, abs=0.000001)
    ), "got wrong forward transform result.(3)"

    # Test RPC_DEMINTERPOLATION=cubic

    tr = gdal.Transformer(
        ds,
        None,
        [
            "METHOD=RPC",
            "RPC_HEIGHT_SCALE=2",
            "RPC_DEM=/vsimem/dem.tif",
            "RPC_DEMINTERPOLATION=cubic",
        ],
    )

    success, pnt = tr.TransformPoint(0, 20.5, 10.5, 0)

    assert (
        success
        and pnt[0] == pytest.approx(125.64828521533849, abs=0.000001)
        and pnt[1] == pytest.approx(39.869345204440144, abs=0.000001)
    ), "got wrong forward transform result.(5)"

    success, pnt = tr.TransformPoint(1, pnt[0], pnt[1], pnt[2])

    assert (
        success
        and pnt[0] == pytest.approx(20.5, abs=0.05)
        and pnt[1] == pytest.approx(10.5, abs=0.05)
    ), "got wrong reverse transform result.(5)"

    tr = None

    # Test RPC_DEMINTERPOLATION=near

    tr = gdal.Transformer(
        ds,
        None,
        [
            "METHOD=RPC",
            "RPC_HEIGHT_SCALE=2",
            "RPC_DEM=/vsimem/dem.tif",
            "RPC_DEMINTERPOLATION=near",
        ],
    )

    success, pnt = tr.TransformPoint(0, 20.5, 10.5, 0)

    assert (
        success
        and pnt[0] == pytest.approx(125.64828521503811, abs=0.000001)
        and pnt[1] == pytest.approx(39.869345204874911, abs=0.000001)
    ), "got wrong forward transform result.(6)"

    success, pnt = tr.TransformPoint(1, pnt[0], pnt[1], pnt[2])

    assert (
        success
        and pnt[0] == pytest.approx(20.5, abs=0.05)
        and pnt[1] == pytest.approx(10.5, abs=0.05)
    ), "got wrong reverse transform result.(6)"

    tr = None

    # Test outside DEM extent : default behaviour --> error
    tr = gdal.Transformer(
        ds, None, ["METHOD=RPC", "RPC_HEIGHT_SCALE=2", "RPC_DEM=/vsimem/dem.tif"]
    )

    success, pnt = tr.TransformPoint(0, 40000, 0, 0)
    assert success == 0

    success, pnt = tr.TransformPoint(1, 125, 40, 0)
    assert success == 0

    tr = None

    # Test outside DEM extent with RPC_DEM_MISSING_VALUE=0
    ds_dem = gdal.GetDriverByName("GTiff").Create("/vsimem/dem.tif", 100, 100, 1)
    sr = osr.SpatialReference()
    sr.ImportFromEPSG(32652)
    ds_dem.SetProjection(sr.ExportToWkt())
    ds_dem.SetGeoTransform([213300, 1, 0, 4418700, 0, -1])
    ds_dem.GetRasterBand(1).Fill(15)
    ds_dem = None
    tr = gdal.Transformer(
        ds,
        None,
        [
            "METHOD=RPC",
            "RPC_HEIGHT_SCALE=2",
            "RPC_DEM=/vsimem/dem.tif",
            "RPC_DEM_MISSING_VALUE=0",
        ],
    )

    success, pnt = tr.TransformPoint(0, -99.5, 0.5, 0)
    assert (
        success
        and pnt[0] == pytest.approx(125.64746155942839, abs=0.000001)
        and pnt[1] == pytest.approx(39.869506789921168, abs=0.000001)
    ), "got wrong forward transform result."

    success, pnt = tr.TransformPoint(1, pnt[0], pnt[1], pnt[2])
    assert (
        success
        and pnt[0] == pytest.approx(-99.5, abs=0.05)
        and pnt[1] == pytest.approx(0.5, abs=0.05)
    ), "got wrong reverse transform result."

    tr = None

    gdal.Unlink("/vsimem/dem.tif")


###############################################################################
# Test RPC convergence bug (bug # 5395)


@pytest.mark.skipif(
    not gdaltest.vrt_has_open_support(),
    reason="VRT driver open missing",
)
def test_transformer_6():

    ds = gdal.Open("data/rpc_5395.vrt")
    tr = gdal.Transformer(ds, None, ["METHOD=RPC"])

    success, pnt = tr.TransformPoint(0, 0.5, 0.5)

    assert (
        success
        and pnt[0] == pytest.approx(28.26163232, abs=0.0001)
        and pnt[1] == pytest.approx(-27.79853245, abs=0.0001)
        and pnt[2] == 0
    ), "got wrong forward transform result."


###############################################################################
# Test Transformer.TransformPoints


def test_transformer_7():

    ds = gdal.Open("data/byte.tif")
    tr = gdal.Transformer(ds, None, [])

    pnt, success = tr.TransformPoints(0, [(20, 10)])

    assert (
        success[0] != 0
        and pnt[0][0] == pytest.approx(441920, abs=0.00000001)
        and pnt[0][1] == pytest.approx(3750720, abs=0.00000001)
        and pnt[0][2] == 0.0
    ), "got wrong forward transform result."


###############################################################################
# Test handling of nodata in RPC DEM (#5680)


@pytest.mark.skipif(
    not gdaltest.vrt_has_open_support(),
    reason="VRT driver open missing",
)
def test_transformer_8():

    ds = gdal.Open("data/rpc.vrt")

    # (long,lat)=(125.64828521533849 39.869345204440144) -> (Easting,Northing)=(213324.662167036 4418634.47813677) in EPSG:32652
    ds_dem = gdal.GetDriverByName("GTiff").Create(
        "/vsimem/dem.tif", 100, 100, 1, gdal.GDT_Int16
    )
    sr = osr.SpatialReference()
    sr.ImportFromEPSG(32652)
    ds_dem.SetProjection(sr.ExportToWkt())
    ds_dem.SetGeoTransform([213300, 1, 0, 4418700, 0, -1])
    ds_dem.GetRasterBand(1).SetNoDataValue(-32768)
    ds_dem.GetRasterBand(1).Fill(-32768)
    ds_dem = None

    for method in ["near", "bilinear", "cubic"]:
        tr = gdal.Transformer(
            ds,
            None,
            [
                "METHOD=RPC",
                "RPC_DEM=/vsimem/dem.tif",
                "RPC_DEMINTERPOLATION=%s" % method,
            ],
        )

        success, pnt = tr.TransformPoint(0, 20, 10, 0)

        if success:
            print(success, pnt)
            pytest.fail("got wrong forward transform result.")

        success, pnt = tr.TransformPoint(1, 125.64828521533849, 39.869345204440144, 0)

        if success:
            print(success, pnt)
            pytest.fail("got wrong reverse transform result.")

    gdal.Unlink("/vsimem/dem.tif")


###############################################################################
# Test RPC DEM line optimization


@pytest.mark.skipif(
    not gdaltest.vrt_has_open_support(),
    reason="VRT driver open missing",
)
def test_transformer_9():

    ds = gdal.Open("data/rpc.vrt")

    # (long,lat)=(125.64828521533849 39.869345204440144) -> (Easting,Northing)=(213324.662167036 4418634.47813677) in EPSG:32652
    ds_dem = gdal.GetDriverByName("GTiff").Create(
        "/vsimem/dem.tif", 100, 100, 1, gdal.GDT_UInt8
    )
    sr = osr.SpatialReference()
    sr.ImportFromEPSG(4326)
    ds_dem.SetProjection(sr.ExportToWkt())
    ds_dem.SetGeoTransform(
        [
            125.647968621436,
            1.2111052640051412e-05,
            0,
            39.869926216038,
            0,
            -8.6569068979969188e-06,
        ]
    )
    import random

    random.seed(0)
    data = "".join([chr(40 + int(10 * random.random())) for _ in range(100 * 100)])
    ds_dem.GetRasterBand(1).WriteRaster(0, 0, 100, 100, data)
    ds_dem = None

    for method in ["near", "bilinear", "cubic"]:
        tr = gdal.Transformer(
            ds,
            None,
            [
                "METHOD=RPC",
                "RPC_DEM=/vsimem/dem.tif",
                "RPC_DEMINTERPOLATION=%s" % method,
            ],
        )

        points = [(125.64828521533849, 39.869345204440144)] * 10
        pnt, success = tr.TransformPoints(1, points)
        assert success[0], method
        pnt_optimized = pnt[0]

        success, pnt = tr.TransformPoint(1, 125.64828521533849, 39.869345204440144, 0)
        assert success, method

        assert pnt == pnt_optimized, method

    gdal.Unlink("/vsimem/dem.tif")


###############################################################################
# Test RPC DEM transform from geoid height to ellipsoidal height


@pytest.mark.skipif(
    not gdaltest.vrt_has_open_support(),
    reason="VRT driver open missing",
)
@pytest.mark.require_driver("GTX")
def test_transformer_10():

    # Create fake vertical shift grid
    out_ds = gdal.GetDriverByName("GTX").Create(
        "tmp/fake.gtx", 10, 10, 1, gdal.GDT_Float32
    )
    out_ds.SetGeoTransform([-180, 36, 0, 90, 0, -18])
    sr = osr.SpatialReference()
    sr.SetWellKnownGeogCS("WGS84")
    out_ds.SetProjection(sr.ExportToWkt())
    out_ds.GetRasterBand(1).Fill(100)
    out_ds = None

    # Create a fake DEM
    ds_dem = gdal.GetDriverByName("GTiff").Create(
        "/vsimem/dem.tif", 100, 100, 1, gdal.GDT_UInt8
    )
    ds_dem.SetGeoTransform(
        [
            125.647968621436,
            1.2111052640051412e-05,
            0,
            39.869926216038,
            0,
            -8.6569068979969188e-06,
        ]
    )
    import random

    random.seed(0)
    data = "".join([chr(40 + int(10 * random.random())) for _ in range(100 * 100)])
    ds_dem.GetRasterBand(1).WriteRaster(0, 0, 100, 100, data)
    ds_dem = None

    ds_dem = gdal.Open("/vsimem/dem.tif")
    vrt_dem = gdal.GetDriverByName("VRT").CreateCopy("/vsimem/dem.vrt", ds_dem)
    ds_dem = None

    vrt_dem.SetProjection("""COMPD_CS["WGS 84 + my_height",
    GEOGCS["WGS 84",
        DATUM["WGS_1984",
            SPHEROID["WGS 84",6378137,298.257223563,
                AUTHORITY["EPSG","7030"]],
            AUTHORITY["EPSG","6326"]],
        PRIMEM["Greenwich",0,
            AUTHORITY["EPSG","8901"]],
        UNIT["degree",0.0174532925199433,
            AUTHORITY["EPSG","9122"]],
        AUTHORITY["EPSG","4326"]],
    VERT_CS["my_height",
        VERT_DATUM["my_height",0,
            EXTENSION["PROJ4_GRIDS","./tmp/fake.gtx"]],
        UNIT["metre",1,
            AUTHORITY["EPSG","9001"]],
        AXIS["Up",UP]]]""")
    vrt_dem = None

    ds = gdal.Open("data/rpc.vrt")

    tr = gdal.Transformer(ds, None, ["METHOD=RPC", "RPC_DEM=/vsimem/dem.vrt"])
    success, pnt = tr.TransformPoint(1, 125.64828521533849, 39.869345204440144, 0)
    assert (
        success
        and pnt[0] == pytest.approx(27.31476045569616, abs=1e-5)
        and pnt[1] == pytest.approx(-53.328814757762302, abs=1e-5)
        and pnt[2] == 0
    ), "got wrong result: %s" % str(pnt)

    tr = gdal.Transformer(
        ds,
        None,
        ["METHOD=RPC", "RPC_DEM=/vsimem/dem.vrt", "RPC_DEM_APPLY_VDATUM_SHIFT=FALSE"],
    )
    success, pnt = tr.TransformPoint(1, 125.64828521533849, 39.869345204440144, 0)

    assert (
        success
        and pnt[0] == pytest.approx(21.445626206892484, abs=1e-5)
        and pnt[1] == pytest.approx(1.6460100520871492, abs=1e-5)
        and pnt[2] == 0
    ), "got wrong result."

    gdal.GetDriverByName("GTX").Delete("tmp/fake.gtx")
    gdal.Unlink("/vsimem/dem.tif")
    gdal.Unlink("/vsimem/dem.vrt")


###############################################################################
# Test failed inverse RPC transform (#6162)


def test_transformer_11():

    ds = gdal.GetDriverByName("MEM").Create("", 6600, 4400)
    rpc = [
        "HEIGHT_OFF=1113.66579196784",
        "HEIGHT_SCALE=12.5010114250099",
        "LAT_OFF=38.8489906468112",
        "LAT_SCALE=-0.106514418771489",
        "LINE_DEN_COEFF=1 -0.000147949809772754 -0.000395269640841174 -1.15825619524758e-05 -0.001613476071797 5.20818134468044e-05 -2.87546958936308e-05 0.00139252754800089 0.00103224907048726 -5.0328770407996e-06 8.03722313022155e-06 0.000236052289425919 0.000208478107633822 -8.11629138727222e-06 0.000168941442517399 0.000392113144410504 3.13299811375497e-06 -1.50306451132806e-07 -1.96870155855449e-06 6.84425679628047e-07",
        "LINE_NUM_COEFF=0.00175958077249233 1.38380980570961 -1.10937056344449 -2.64222540811728e-05 0.00242330787142254 0.000193743606261641 -0.000149740797138056 0.000348558508286103 -8.44646294793856e-05 3.10853483444725e-05 6.94899990982205e-05 -0.00348125387930033 -0.00481553689971959 -7.80038440894703e-06 0.00410332555882184 0.00269594666059233 5.94355882183947e-06 -6.12499223746471e-05 -2.16490482825638e-05 -1.95059491792213e-06",
        "LINE_OFF=2199.80872158044",
        "LINE_SCALE=2202.03966104116",
        "LONG_OFF=77.3374268058015",
        "LONG_SCALE=0.139483831686384",
        "SAMP_DEN_COEFF=1 0.000220381598198686 -5.9113079248377e-05 -0.000123013508187712 -2.69270454504924e-05 3.85090208529735e-05 -5.05359221990966e-05 0.000207017095461956 0.000441092857548974 1.47302072491805e-05 9.4840973108768e-06 -0.000810344094204395 -0.000690502911945615 -1.07959445293954e-05 0.000801157109076503 0.000462754838815978 9.13256389877791e-06 7.49571761868177e-06 -5.00612460432453e-06 -2.25925949180435e-06",
        "SAMP_NUM_COEFF=-0.00209214639511201 -0.759096012299728 -0.903450038473527 5.43928095403867e-05 -0.000717672934172181 -0.000168790405106395 -0.00015564609496447 0.0013261576802665 -0.000398331147368139 -3.84712681506314e-05 2.70041394522796e-05 0.00254362585790201 0.00332988183285888 3.36326833370395e-05 0.00445687297094153 0.00290078876854111 3.59552237739047e-05 7.16492495304347e-05 -5.6782194494005e-05 2.32051448455541e-06",
        "SAMP_OFF=3300.39818049514",
        "SAMP_SCALE=3302.50400920438",
    ]
    ds.SetMetadata(rpc, "RPC")

    tr = gdal.Transformer(ds, None, ["METHOD=RPC", "RPC_HEIGHT=4000"])
    success, pnt = tr.TransformPoint(0, 0, 0, 0)
    assert not success, pnt

    # But this one should succeed
    tr = gdal.Transformer(ds, None, ["METHOD=RPC", "RPC_HEIGHT=1150"])
    success, pnt = tr.TransformPoint(0, 0, 0, 0)
    assert (
        success
        and pnt[0] == pytest.approx(77.350939956024618, abs=1e-7)
        and pnt[1] == pytest.approx(38.739703990877814, abs=1e-7)
    )


###############################################################################
# Test degenerate cases of TPS transformer


@pytest.mark.skipif(
    not gdaltest.vrt_has_open_support(),
    reason="VRT driver open missing",
)
def test_transformer_12():

    ds = gdal.Open("""
    <VRTDataset rasterXSize="20" rasterYSize="20">
  <GCPList Projection="PROJCS[&quot;NAD27 / UTM zone 11N&quot;,GEOGCS[&quot;NAD27&quot;,DATUM[&quot;North_American_Datum_1927&quot;,SPHEROID[&quot;Clarke 1866&quot;,6378206.4,294.9786982139006,AUTHORITY[&quot;EPSG&quot;,&quot;7008&quot;]],AUTHORITY[&quot;EPSG&quot;,&quot;6267&quot;]],PRIMEM[&quot;Greenwich&quot;,0],UNIT[&quot;degree&quot;,0.0174532925199433],AUTHORITY[&quot;EPSG&quot;,&quot;4267&quot;]],PROJECTION[&quot;Transverse_Mercator&quot;],PARAMETER[&quot;latitude_of_origin&quot;,0],PARAMETER[&quot;central_meridian&quot;,-117],PARAMETER[&quot;scale_factor&quot;,0.9996],PARAMETER[&quot;false_easting&quot;,500000],PARAMETER[&quot;false_northing&quot;,0],UNIT[&quot;metre&quot;,1,AUTHORITY[&quot;EPSG&quot;,&quot;9001&quot;]],AUTHORITY[&quot;EPSG&quot;,&quot;26711&quot;]]">
    <GCP Id="" Pixel="0" Line="0" X="-1" Y="-1"/>
    <GCP Id="" Pixel="20" Line="0" X="1" Y="-1"/>
    <GCP Id="" Pixel="0" Line="20" X="-1" Y="1"/>
    <GCP Id="" Pixel="20" Line="20" X="1" Y="1"/>
    <GCP Id="" Pixel="0" Line="0" X="-1" Y="-1"/> <!-- duplicate entry -->
  </GCPList>
  <VRTRasterBand dataType="Byte" band="1">
    <ColorInterp>Gray</ColorInterp>
    <SimpleSource>
      <SourceFilename relativeToVRT="1">data/byte.tif</SourceFilename>
    </SimpleSource>
  </VRTRasterBand>
</VRTDataset>""")

    tr = gdal.Transformer(ds, None, ["METHOD=GCP_TPS"])
    assert tr is not None
    success, pnt = tr.TransformPoint(0, 0, 0)
    assert (
        success
        and pnt[0] == pytest.approx(-1, abs=1e-7)
        and pnt[1] == pytest.approx(-1, abs=1e-7)
    )

    success, pnt = tr.TransformPoint(1, -1, -1)
    assert (
        success
        and pnt[0] == pytest.approx(0, abs=1e-7)
        and pnt[1] == pytest.approx(0, abs=1e-7)
    )

    success, pnt = tr.TransformPoint(1, 0.2, 0.8)
    assert (
        success
        and pnt[0] == pytest.approx(12, abs=1e-7)
        and pnt[1] == pytest.approx(18, abs=1e-7)
    )

    ds = gdal.Open("""
    <VRTDataset rasterXSize="20" rasterYSize="20">
  <GCPList Projection="PROJCS[&quot;NAD27 / UTM zone 11N&quot;,GEOGCS[&quot;NAD27&quot;,DATUM[&quot;North_American_Datum_1927&quot;,SPHEROID[&quot;Clarke 1866&quot;,6378206.4,294.9786982139006,AUTHORITY[&quot;EPSG&quot;,&quot;7008&quot;]],AUTHORITY[&quot;EPSG&quot;,&quot;6267&quot;]],PRIMEM[&quot;Greenwich&quot;,0],UNIT[&quot;degree&quot;,0.0174532925199433],AUTHORITY[&quot;EPSG&quot;,&quot;4267&quot;]],PROJECTION[&quot;Transverse_Mercator&quot;],PARAMETER[&quot;latitude_of_origin&quot;,0],PARAMETER[&quot;central_meridian&quot;,-117],PARAMETER[&quot;scale_factor&quot;,0.9996],PARAMETER[&quot;false_easting&quot;,500000],PARAMETER[&quot;false_northing&quot;,0],UNIT[&quot;metre&quot;,1,AUTHORITY[&quot;EPSG&quot;,&quot;9001&quot;]],AUTHORITY[&quot;EPSG&quot;,&quot;26711&quot;]]">
    <GCP Id="" Pixel="0" Line="0" X="0" Y="0"/>
    <GCP Id="" Pixel="20" Line="0" X="20" Y="0"/>
    <GCP Id="" Pixel="0" Line="20" X="0" Y="20"/>
    <GCP Id="" Pixel="20" Line="20" X="20" Y="20"/>
    <GCP Id="" Pixel="0" Line="0" X="10" Y="10"/> <!-- same pixel,line -->
  </GCPList>
  <VRTRasterBand dataType="Byte" band="1">
    <ColorInterp>Gray</ColorInterp>
    <SimpleSource>
      <SourceFilename relativeToVRT="1">data/byte.tif</SourceFilename>
    </SimpleSource>
  </VRTRasterBand>
</VRTDataset>""")

    gdal.ErrorReset()
    with gdaltest.disable_exceptions(), gdaltest.error_handler():
        tr = gdal.Transformer(ds, None, ["METHOD=GCP_TPS"])
        assert gdal.GetLastErrorMsg() != ""

    ds = gdal.Open("""
    <VRTDataset rasterXSize="20" rasterYSize="20">
  <GCPList Projection="PROJCS[&quot;NAD27 / UTM zone 11N&quot;,GEOGCS[&quot;NAD27&quot;,DATUM[&quot;North_American_Datum_1927&quot;,SPHEROID[&quot;Clarke 1866&quot;,6378206.4,294.9786982139006,AUTHORITY[&quot;EPSG&quot;,&quot;7008&quot;]],AUTHORITY[&quot;EPSG&quot;,&quot;6267&quot;]],PRIMEM[&quot;Greenwich&quot;,0],UNIT[&quot;degree&quot;,0.0174532925199433],AUTHORITY[&quot;EPSG&quot;,&quot;4267&quot;]],PROJECTION[&quot;Transverse_Mercator&quot;],PARAMETER[&quot;latitude_of_origin&quot;,0],PARAMETER[&quot;central_meridian&quot;,-117],PARAMETER[&quot;scale_factor&quot;,0.9996],PARAMETER[&quot;false_easting&quot;,500000],PARAMETER[&quot;false_northing&quot;,0],UNIT[&quot;metre&quot;,1,AUTHORITY[&quot;EPSG&quot;,&quot;9001&quot;]],AUTHORITY[&quot;EPSG&quot;,&quot;26711&quot;]]">
    <GCP Id="" Pixel="0" Line="0" X="0" Y="0"/>
    <GCP Id="" Pixel="20" Line="0" X="20" Y="0"/>
    <GCP Id="" Pixel="0" Line="20" X="0" Y="20"/>
    <GCP Id="" Pixel="20" Line="20" X="20" Y="20"/>
    <GCP Id="" Pixel="10" Line="10" X="20" Y="20"/> <!-- same X,Y -->
  </GCPList>
  <VRTRasterBand dataType="Byte" band="1">
    <ColorInterp>Gray</ColorInterp>
    <SimpleSource>
      <SourceFilename relativeToVRT="1">data/byte.tif</SourceFilename>
    </SimpleSource>
  </VRTRasterBand>
</VRTDataset>""")

    gdal.ErrorReset()
    with gdaltest.disable_exceptions(), gdaltest.error_handler():
        tr = gdal.Transformer(ds, None, ["METHOD=GCP_TPS"])
        assert gdal.GetLastErrorMsg() != ""


###############################################################################
# Test inverse RPC transform at DEM edge (#6377)


def test_transformer_13():

    ds = gdal.GetDriverByName("MEM").Create("", 6600, 4400)
    rpc = [
        "HEIGHT_OFF=79.895358112544",
        "HEIGHT_SCALE=71.8479951519956",
        "LAT_OFF=39.1839631741725",
        "LAT_SCALE=-0.0993355184710674",
        "LINE_DEN_COEFF=1 8.18889582174233e-05 -0.000585027621468826 0.00141894885228522 -0.000585589558894143 2.26848970721562e-05 0.0004556101949561 -0.000807782279739336 -0.00042471862816941 -0.000569244978738162 1.48442578097541e-05 4.05131290592846e-05 2.84884306250279e-05 -5.18205692205965e-06 -6.313878273056e-07 1.53979251356426e-05 -7.18376115203249e-06 -6.17331013601745e-05 -7.21314704472095e-05 4.12297300238455e-06",
        "LINE_NUM_COEFF=-0.00742236358913794 -1.34432796989641 1.14742235955483 -0.00419813954264328 -0.00234215180175534 -0.00624463816085957 0.00678228413157904 0.0020362389986917 -0.00187712244349171 -8.03499198655765e-08 -0.00058862905508099 -0.00738644673656152 -0.00769111767189179 0.00076485216017804 0.00714033152180546 0.00597946564795612 -0.000632882594479344 -0.000167672086277102 0.00055226160003967 1.01784884515205e-06",
        "LINE_OFF=2199.71134437189",
        "LINE_SCALE=2197.27163235171",
        "LONG_OFF=-108.129788954851",
        "LONG_SCALE=0.135395601856691",
        "SAMP_DEN_COEFF=1 -0.000817668457487893 -0.00151956231901818 0.00117149108953055 0.000514723430775277 0.000357856819755055 0.000655430235824068 -0.00100177312999255 -0.000488725013873637 -0.000500795084518271 -3.31511569640467e-06 4.60608554396048e-05 4.71371559254521e-05 -3.47487113243818e-06 1.0984752288197e-05 1.6421626141648e-05 -6.2866141729034e-06 -6.32966599886646e-05 -7.06552514786235e-05 3.89288575686084e-06",
        "SAMP_NUM_COEFF=0.00547379112608157 0.807100297014362 0.845388298829057 0.01082483811889 -0.00320368761068744 0.00357867636379949 0.00459377712275926 -0.00324853865239341 -0.00218177030092682 2.99823054607907e-05 0.000946829367823539 0.00428577519330827 0.0045745876325088 -0.000396201144848935 0.00488772258958395 0.00435309486883759 -0.000402737234433541 0.000402935809278189 0.000642374929382851 -5.26793321752838e-06",
        "SAMP_OFF=3299.3111821927",
        "SAMP_SCALE=3297.19448149873",
    ]
    ds.SetMetadata(rpc, "RPC")

    tr = gdal.Transformer(
        ds, None, ["METHOD=RPC", "RPC_DEM=data/transformer_13_dem.tif"]
    )
    success, pnt = tr.TransformPoint(0, 6600, 24)
    assert (
        success
        and pnt[0] == pytest.approx(-108.00069819119149, abs=1e-7)
        and pnt[1] == pytest.approx(39.15771125604824, abs=1e-7)
    )


###############################################################################
# Test inverse RPC transform when iterations do oscillations (#6377)


def test_transformer_14():
    ds = gdal.GetDriverByName("MEM").Create("", 4032, 2688)
    rpc = [
        "MIN_LAT=0",
        "MAX_LAT=0",
        "MIN_LONG=0",
        "MAX_LONG=0",
        "HEIGHT_OFF=244.72924043124081",
        "HEIGHT_SCALE=391.44066987292678",
        "LAT_OFF=0.095493639758799986",
        "LAT_SCALE=-0.0977494003085103",
        "LINE_DEN_COEFF=1 1.73399671259238e-05 -6.18169396309642e-06 -3.11498839490863e-05 -1.18048814815295e-05 -5.46123898974842e-05 -2.51203895820587e-05 -5.77299008756702e-05 -1.37836923606953e-05 -3.24029327866125e-06 2.06307542696228e-07 -5.16777154466466e-08 2.98762926005741e-07 3.17761145061869e-08 1.48077371641094e-07 -7.69738626480047e-08 2.94990048269861e-08 -3.37468052222007e-08 -3.67859879729462e-08 8.79847359414426e-10 ",
        "LINE_NUM_COEFF=0.000721904493927027 1.02330510505135 -1.27742813759689 -0.0973049949136407 -0.014260789316429 0.00229308399354221 -0.0016640916975237 0.0124508639909873 0.00336835383694126 1.1987123734283e-05 -1.85240614830659e-05 4.40716454954686e-05 2.3198555492418e-05 -8.31659287301587e-08 -5.10329082923063e-05 2.56477008932482e-05 1.01465909326012e-05 1.04407036240869e-05 4.27413648628578e-05 2.91696764503125e-07 ",
        "LINE_OFF=1343.99369782095",
        "LINE_SCALE=1343.96638400536",
        "LONG_OFF=-0.034423410000698595",
        "LONG_SCALE=0.143444599019706",
        "SAMP_DEN_COEFF=1 1.83636704399141e-05 3.55794197969218e-06 -1.33255440425932e-05 -4.25424777986987e-06 -3.95287146748821e-05 1.35786181318561e-05 -3.86131208639696e-05 -1.10085128708761e-05 -1.26863939055319e-05 -2.88045902675552e-07 -1.58732907217101e-07 4.08999884183478e-07 6.6854211618061e-08 -1.46399266323942e-07 -4.69718293745237e-08 -4.14626818788491e-08 -3.00588241056424e-07 4.54784506604435e-08 3.24214474149225e-08 ",
        "SAMP_NUM_COEFF=-0.0112062780844554 -1.05096833835297 -0.704023055461029 0.0384547265206585 -0.00987134340336078 -0.00310989611092616 -0.00116937850565916 -0.0102714370609919 0.000930565787504389 7.03834691339565e-05 -3.83216250787844e-05 -3.67841179314918e-05 2.45498653278515e-05 1.06302833544472e-05 -6.26921822677631e-05 1.29769009118128e-05 1.1336284460811e-05 -3.01250967502161e-05 -7.60511798099513e-06 -4.45260900205512e-07 ",
        "SAMP_OFF=2015.99417232167",
        "SAMP_SCALE=2015.9777295656",
    ]
    ds.SetMetadata(rpc, "RPC")

    with gdal.config_options(
        {"RPC_INVERSE_VERBOSE": "YES", "RPC_INVERSE_LOG": "/vsimem/transformer_14.csv"}
    ):
        tr = gdal.Transformer(
            ds, None, ["METHOD=RPC", "RPC_DEM=data/transformer_14_dem.tif"]
        )
    success, pnt = tr.TransformPoint(0, 45, 73)
    # on debug it should say this two messages
    # Oscillation detected...
    # Converged!
    assert (
        success
        and pnt[0] == pytest.approx(0.000801360096912016, abs=1e-7)
        and pnt[1] == pytest.approx(-4.1346931131054286e-05, abs=1e-7)
    )

    f = gdal.VSIFOpenL("/vsimem/transformer_14.csvt", "rb")
    if f is not None:
        content = gdal.VSIFReadL(1, 1000, f).decode("ASCII")
        gdal.VSIFCloseL(f)
    assert content.startswith("Integer,Real,Real,Real,String,Real,Real")

    f = gdal.VSIFOpenL("/vsimem/transformer_14.csv", "rb")
    if f is not None:
        content = gdal.VSIFReadL(1, 1000, f).decode("ASCII")
        gdal.VSIFCloseL(f)
    assert content.startswith("""iter,long,lat,height,WKT,error_pixel_x,error_pixel_y
0,""")

    gdal.Unlink("/vsimem/transformer_14.csvt")
    gdal.Unlink("/vsimem/transformer_14.csv")


###############################################################################
# Test inverse RPC transform with DEM in [-180,180] but guessed longitude going
# beyond


@pytest.mark.skipif(
    not gdaltest.vrt_has_open_support(),
    reason="VRT driver open missing",
)
def test_transformer_15():

    ds = gdal.GetDriverByName("MEM").Create("", 6600, 4400)
    rpc = [
        "HEIGHT_OFF=50",
        "HEIGHT_SCALE=10",
        "LAT_OFF=0",
        "LAT_SCALE=-0.105215174097221",
        "LINE_DEN_COEFF=1 0.000113241782375585 -7.43522609681362e-05 3.71535308900828e-08 0.000338102551252493 4.57912749279076e-07 -7.00823537484445e-08 9.01275640119332e-05 -0.000209723741981335 4.74972505083506e-09 7.2855438070338e-10 -3.16688523275456e-05 -4.49211037453489e-05 8.29706981496464e-12 7.58128162744879e-06 6.82536481272507e-07 2.58661007069147e-11 -3.9697791887986e-08 -5.06502821928986e-08 4.66978771069271e-11 ",
        "LINE_NUM_COEFF=0.00673418095252569 -1.38985626744028 -0.645141238074041 1.4661564574111e-05 0.00022202101663831 -4.32910472926433e-06 -1.90048143949724e-06 -0.00374486341484218 -0.00041396053769863 1.14148334846788e-09 -1.20458144309064e-07 -0.00618122456017927 -0.00783023711720404 1.32704635552568e-09 -0.0032532225011433 -0.00355239507036451 6.20315160857432e-10 -4.75170167074672e-07 1.37652348819162e-07 9.53393990126859e-12 ",
        "LINE_OFF=2191.17012189569",
        "LINE_SCALE=2197.23749680132",
        "LONG_OFF=179.9",
        "LONG_SCALE=0.124705315627701",
        "SAMP_DEN_COEFF=1 2.1759243891474e-05 -5.96367842636917e-06 6.03714873685079e-09 -0.000109764869260479 4.67786228725161e-07 -4.22013004308237e-07 0.000110379633112327 -0.00011679427405351 9.30515192365439e-09 1.09243100743555e-09 -3.29956656106618e-05 -4.1918167733603e-05 2.78076356769406e-11 -4.57011012884097e-06 -1.01453844685279e-05 2.60244039950836e-11 -3.52960090220746e-08 -4.49975439524006e-08 1.80641651820794e-11 ",
        "SAMP_NUM_COEFF=-0.000776008179693209 -0.380692820465227 1.0647540743599 1.72660888229082e-06 0.0028311896140173 -1.0466632896253e-06 3.17728359468736e-06 -0.000150466834838251 0.000566231304705376 -7.34786296240808e-11 5.90340934437437e-07 -0.00169917056998316 -0.00231193494979752 -2.71280972480264e-09 0.00482224415396017 0.00596164573794891 7.58464598771136e-09 -1.46075167736497e-07 -4.64652272450397e-07 -5.59268858101854e-13 ",
        "SAMP_OFF=3300.0420831968",
        "SAMP_SCALE=3295.90302088781",
    ]
    ds.SetMetadata(rpc, "RPC")

    sr = osr.SpatialReference()
    sr.SetWellKnownGeogCS("WGS84")
    demE179 = gdal.GetDriverByName("GTiff").Create("/vsimem/demE179.tif", 10, 10)
    demE179.SetProjection(sr.ExportToWkt())
    demE179.SetGeoTransform([179, 0.1, 0, 0.5, 0, -0.1])
    demE179.GetRasterBand(1).Fill(50)
    demW180 = gdal.GetDriverByName("GTiff").Create("/vsimem/demW180.tif", 10, 10)
    demW180.SetProjection(sr.ExportToWkt())
    demW180.SetGeoTransform([-180, 0.1, 0, 0.5, 0, -0.1])
    demW180.GetRasterBand(1).Fill(50)
    gdal.BuildVRT("/vsimem/transformer_15_dem.vrt", [demE179, demW180])
    demE179 = None
    demW180 = None

    tr = gdal.Transformer(
        ds, None, ["METHOD=RPC", "RPC_DEM=/vsimem/transformer_15_dem.vrt"]
    )
    success, pnt = tr.TransformPoint(0, 0, 0)
    assert (
        success
        and pnt[0] == pytest.approx(180.02280735469199, abs=1e-7)
        and pnt[1] == pytest.approx(0.061069145746997976, abs=1e-7)
    )

    success, pnt_forward = tr.TransformPoint(1, pnt[0], pnt[1], 0)
    assert (
        success
        and pnt_forward[0] == pytest.approx(0, abs=0.1)
        and pnt_forward[1] == pytest.approx(0, abs=0.1)
    ), "got wrong reverse transform result."

    success, pnt_forward = tr.TransformPoint(1, pnt[0] - 360, pnt[1], 0)
    assert (
        success
        and pnt_forward[0] == pytest.approx(0, abs=0.1)
        and pnt_forward[1] == pytest.approx(0, abs=0.1)
    ), "got wrong reverse transform result."

    # Now test around -180
    ds = gdal.GetDriverByName("MEM").Create("", 6600, 4400)
    rpc.remove("LONG_OFF=179.9")
    rpc += ["LONG_OFF=-179.9"]
    ds.SetMetadata(rpc, "RPC")

    tr = gdal.Transformer(
        ds, None, ["METHOD=RPC", "RPC_DEM=/vsimem/transformer_15_dem.vrt"]
    )
    success, pnt = tr.TransformPoint(0, 6600, 4400)
    assert (
        success
        and pnt[0] == pytest.approx(-180.02313813793387, abs=1e-7)
        and pnt[1] == pytest.approx(-0.061398913932229765, abs=1e-7)
    )

    success, pnt_forward = tr.TransformPoint(1, pnt[0], pnt[1], 0)
    assert (
        success
        and pnt_forward[0] == pytest.approx(6600, abs=0.1)
        and pnt_forward[1] == pytest.approx(4400, abs=0.1)
    ), "got wrong reverse transform result."

    success, pnt_forward = tr.TransformPoint(1, pnt[0] + 360, pnt[1], 0)
    assert (
        success
        and pnt_forward[0] == pytest.approx(6600, abs=0.1)
        and pnt_forward[1] == pytest.approx(4400, abs=0.1)
    ), "got wrong reverse transform result."

    gdal.Unlink("/vsimem/demE179.tif")
    gdal.Unlink("/vsimem/demW180.tif")
    gdal.Unlink("/vsimem/transformer_15_dem.vrt")


###############################################################################
# Test approximate sub-transformers in GenImgProjTransformer
# (we mostly test that the parameters are well recognized and serialized)


@pytest.mark.skipif(
    not gdaltest.vrt_has_open_support(),
    reason="VRT driver open missing",
)
def test_transformer_16():

    gdal.Translate(
        "/vsimem/transformer_16.tif",
        "data/byte.tif",
        options="-gcp 0 0 440720.000 3751320.000 -gcp 0 20 440720.000 3750120.000 -gcp 20 0 441920.000 3751320.000 -gcp 20 20 441920.000 3750120.000 -a_srs EPSG:26711",
    )
    gdal.Warp(
        "/vsimem/transformer_16.vrt",
        "/vsimem/transformer_16.tif",
        options="-of VRT -t_srs EPSG:4326 -et 0 -to SRC_APPROX_ERROR_IN_SRS_UNIT=6.05 -to SRC_APPROX_ERROR_IN_PIXEL=0.1 -to REPROJECTION_APPROX_ERROR_IN_SRC_SRS_UNIT=6.1 -to REPROJECTION_APPROX_ERROR_IN_DST_SRS_UNIT=0.0001",
    )
    f = gdal.VSIFOpenL("/vsimem/transformer_16.vrt", "rb")
    if f is not None:
        content = gdal.VSIFReadL(1, 10000, f).decode("ASCII")
        gdal.VSIFCloseL(f)
    assert (
        "<MaxErrorForward>6.05</MaxErrorForward>" in content
        and "<MaxErrorReverse>0.1</MaxErrorReverse>" in content
        and "<MaxErrorForward>0.0001</MaxErrorForward>" in content
        and "<MaxErrorReverse>6.1</MaxErrorReverse>" in content
    )
    ds = gdal.Translate("", "/vsimem/transformer_16.vrt", format="MEM")
    assert ds.GetRasterBand(1).Checksum() == 4727
    ds = None
    gdal.Unlink("/vsimem/transformer_16.tif")
    gdal.Unlink("/vsimem/transformer_16.vrt")


###############################################################################
# Test RPC DEM with unexisting RPC DEM file


@pytest.mark.skipif(
    not gdaltest.vrt_has_open_support(),
    reason="VRT driver open missing",
)
def test_transformer_17():

    ds = gdal.Open("data/rpc.vrt")
    with pytest.raises(Exception):
        gdal.Transformer(
            ds, None, ["METHOD=RPC", "RPC_DEM=/vsimem/i/donot/exist/dem.tif"]
        )


def test_transformer_longlat_wrap_outside_180():

    ds = gdal.GetDriverByName("MEM").Create("", 360, 1, 1)
    sr = osr.SpatialReference()
    sr.ImportFromEPSG(4326)
    ds.SetProjection(sr.ExportToWkt())
    ds.SetGeoTransform([-180, 1, 0, 0, 0, -1])
    tr = gdal.Transformer(ds, ds, [])

    success, pnt = tr.TransformPoint(0, -0.5, 0.5, 0)
    assert success
    assert pnt[0] == pytest.approx(359.5, abs=0.000001), pnt
    assert pnt[1] == pytest.approx(0.5, abs=0.000001), pnt


###############################################################################
# Test reprojection transformer without reverse path
# NOTE: in case the inverse airy method is implemented some day, this test
# might fail


@pytest.mark.require_proj(8, 0, 0)
def test_transformer_no_reverse_method():
    tr = gdal.Transformer(
        None,
        None,
        ["SRC_SRS=+proj=longlat +ellps=GRS80", "DST_SRS=+proj=airy +ellps=GRS80"],
    )
    assert tr

    success, pnt = tr.TransformPoint(0, 2, 49)
    assert success
    assert pnt[0] == pytest.approx(141270.54731856665, abs=1e-3), pnt
    assert pnt[1] == pytest.approx(4656605.104980032, abs=1e-3), pnt

    with pytest.raises(Exception, match="No inverse operation"):
        tr.TransformPoint(1, 2, 49)


###############################################################################
# Test precision of GCP based transformer with thin plate splines and lots of GCPs (2115).


@pytest.mark.skipif(
    not gdaltest.vrt_has_open_support(),
    reason="VRT driver open missing",
)
def test_transformer_tps_precision():

    ds = gdal.Open("data/gcps_2115.vrt")
    tr = gdal.Transformer(ds, None, ["METHOD=GCP_TPS"])
    assert tr, "tps transformation could not be computed"

    success = True
    maxDiffResult = 0.0
    gcps = ds.GetGCPs()
    for i, gcp in enumerate(gcps):
        s, result = tr.TransformPoint(0, gcp.GCPPixel, gcp.GCPLine)
        success &= s
        diffResult = math.sqrt(
            (gcp.GCPX - result[0]) ** 2 + (gcp.GCPY - result[1]) ** 2
        )
        maxDiffResult = max(maxDiffResult, diffResult)

        s, result = tr.TransformPoint(1, result[0], result[1])
        assert s
        # The test fails on point 172 only with ICC 2024.0.2.29
        if i not in (172, 1639):
            assert result[0] == pytest.approx(gcp.GCPPixel, rel=1e-5), (i, result)
            assert result[1] == pytest.approx(gcp.GCPLine, rel=1e-5), (i, result)

        if i + 1 < len(gcps):
            # Try transforming "random" points
            xIn = 0.5 * (gcps[i].GCPPixel + gcps[i + 1].GCPPixel)
            yIn = 0.5 * (gcps[i].GCPLine + gcps[i + 1].GCPLine)
            s, result = tr.TransformPoint(0, xIn, yIn)
            assert s
            s, result = tr.TransformPoint(1, result[0], result[1])
            assert s
            # A few points fail to converge with reasonable accuracy
            if i not in (
                775,
                776,
                777,
                784,
                785,
                786,
                787,
                802,
                813,
                814,
                1007,
                1008,
                1009,
                1984,
                1010,
                1254,
                1558,
                1634,
                1638,
                1957,
            ):
                assert result[0] == pytest.approx(xIn, abs=1e-3), (i, xIn, yIn, result)
                assert result[1] == pytest.approx(yIn, abs=1e-3), (i, xIn, yIn, result)

    assert success, "at least one point could not be transformed"
    assert maxDiffResult < 1e-3, "at least one transformation exceeds the error bound"


###############################################################################
def test_transformer_image_no_srs():

    ds = gdal.GetDriverByName("MEM").Create("", 1, 1)
    ds.SetGeoTransform([0, 10, 0, 0, 0, -10])
    tr = gdal.Transformer(
        ds, None, ["COORDINATE_OPERATION=+proj=unitconvert +xy_in=1 +xy_out=2"]
    )
    assert tr
    success, pnt = tr.TransformPoint(0, 10, 20, 0)
    assert success
    assert pnt[0] == pytest.approx(50), pnt
    assert pnt[1] == pytest.approx(-100), pnt


###############################################################################
# Test RPC_DEM_SRS by adding vertical component egm 96 geoid


@pytest.mark.skipif(
    not gdaltest.vrt_has_open_support(),
    reason="VRT driver open missing",
)
def test_transformer_dem_overrride_srs():
    ds = gdal.Open("data/rpc.vrt")
    ds_dem = gdal.GetDriverByName("GTiff").Create("/vsimem/dem.tif", 100, 100, 1)
    sr = osr.SpatialReference()
    sr.ImportFromEPSG(32652)
    ds_dem.SetProjection(sr.ExportToWkt())
    ds_dem.SetGeoTransform([213300, 200, 0, 4418700, 0, -200])
    ds_dem.GetRasterBand(1).Fill(15)
    ds_dem = None
    tr = gdal.Transformer(
        ds,
        None,
        [
            "METHOD=RPC",
            "RPC_HEIGHT_SCALE=2",
            "RPC_DEM=/vsimem/dem.tif",
            "RPC_DEM_SRS=EPSG:32652+5773",
        ],
    )

    success, pnt = tr.TransformPoint(0, 0.5, 0.5, 0)
    assert (
        success
        and pnt[0] == pytest.approx(125.64813723085801, abs=1e-4)
        and pnt[1] == pytest.approx(39.869345977927146, abs=1e-4)
    ), "got wrong forward transform result."

    success, pnt = tr.TransformPoint(1, pnt[0], pnt[1], pnt[2])
    assert (
        success
        and pnt[0] == pytest.approx(0.5, abs=0.05)
        and pnt[1] == pytest.approx(0.5, abs=0.05)
    ), "got wrong reverse transform result."

    gdal.Unlink("/vsimem/dem.tif")


###############################################################################
# Test gdal.SuggestedWarpOutput


def test_transformer_SuggestedWarpOutput_from_Transformer():

    ds = gdal.Open("data/byte.tif")
    tr = gdal.Transformer(ds, None, ["DST_SRS=EPSG:4267"])
    res = gdal.SuggestedWarpOutput(ds, tr)
    assert res.width == 22
    assert res.height == 18
    assert (res.xmin, res.ymin, res.xmax, res.ymax) == pytest.approx(
        (-117.64116862079689, 33.8916535473944, -117.62801015859763, 33.902419561921064)
    )
    assert res.geotransform == pytest.approx(
        (
            -117.64116862079689,
            0.0005981119181481631,
            0.0,
            33.902419561921064,
            0.0,
            -0.0005981119181481631,
        )
    )


###############################################################################
# Test gdal.SuggestedWarpOutput


def test_transformer_SuggestedWarpOutput_from_options():

    ds = gdal.Open("data/byte.tif")
    res = gdal.SuggestedWarpOutput(ds, ["DST_SRS=EPSG:4267"])
    assert res.width == 22
    assert res.height == 18
    assert (res.xmin, res.ymin, res.xmax, res.ymax) == pytest.approx(
        (-117.64116862079689, 33.8916535473944, -117.62801015859763, 33.902419561921064)
    )
    assert res.geotransform == pytest.approx(
        (
            -117.64116862079689,
            0.0005981119181481631,
            0.0,
            33.902419561921064,
            0.0,
            -0.0005981119181481631,
        )
    )


###############################################################################
# Test GCP antimerdian unwrap (https://github.com/OSGeo/gdal/issues/8371)


@pytest.mark.skipif(
    not gdaltest.vrt_has_open_support(),
    reason="VRT driver open missing",
)
def test_transformer_gcp_antimeridian_unwrap():

    ds = gdal.Open("data/test_gcp_antimeridian_unwrap.vrt")

    # Default GCP transformer
    tr = gdal.Transformer(ds, None, [])

    success, pnt = tr.TransformPoint(0, 0, 0)
    assert success and pnt == pytest.approx((180.6645950186466, 62.45182290696794, 0.0))

    success, pnt = tr.TransformPoint(0, 2525, 0)
    assert success and pnt == pytest.approx((175.82001802587033, 62.884780066613, 0.0))

    # TPS transformer
    tr = gdal.Transformer(ds, None, ["METHOD=GCP_TPS"])

    success, pnt = tr.TransformPoint(0, 0, 0)
    assert success and pnt == pytest.approx(
        (180.66674233559996, 62.451179551130025, 0.0)
    )

    success, pnt = tr.TransformPoint(0, 2525, 0)
    assert success and pnt == pytest.approx((175.8219413653614, 62.8840814418221, 0.0))

    tr = gdal.Transformer(ds, None, ["GCP_ANTIMERIDIAN_UNWRAP=YES"])

    success, pnt = tr.TransformPoint(0, 0, 0)
    assert success and pnt == pytest.approx((180.6645950186466, 62.45182290696794, 0.0))

    # Leads to bad result
    tr = gdal.Transformer(ds, None, ["GCP_ANTIMERIDIAN_UNWRAP=NO"])

    success, pnt = tr.TransformPoint(0, 0, 0)
    assert success and pnt == pytest.approx(
        (-97.99753079934052, 62.45182290696794, 0.0)
    )


###############################################################################
# Test passing an unknown transformer option.


@gdaltest.disable_exceptions()
def test_transformer_unknown_option():

    ds = gdal.Open("data/byte.tif")
    with gdal.quiet_errors():
        gdal.ErrorReset()
        gdal.Transformer(ds, None, ["FOO=BAR"])
        assert (
            gdal.GetLastErrorMsg() == "transformer options does not support option FOO"
        )


###############################################################################


def test_transformer_validate_options():

    if (
        gdaltest.is_travis_branch("mingw64")
        or gdaltest.is_travis_branch("build-windows-conda")
        or gdaltest.is_travis_branch("build-windows-minimum")
    ):
        pytest.skip("Crashes for unknown reason")

    from lxml import etree

    schema_optionlist = etree.XML(r"""
<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:element name="Value">
    <xs:complexType>
      <xs:simpleContent>
        <xs:extension base="xs:string">
          <xs:attribute type="xs:string" name="alias" use="optional"/>
        </xs:extension>
      </xs:simpleContent>
    </xs:complexType>
  </xs:element>
  <xs:element name="Option">
    <xs:complexType mixed="true">
      <xs:sequence>
        <xs:element ref="Value" maxOccurs="unbounded" minOccurs="0"/>
      </xs:sequence>
      <xs:attribute name="name" use="required">
        <xs:simpleType>
          <xs:restriction base="xs:string">
            <xs:pattern value="[^\s]*"/>
          </xs:restriction>
        </xs:simpleType>
      </xs:attribute>
      <xs:attribute name="type" use="required">
        <xs:simpleType>
          <xs:restriction base="xs:string">
            <xs:enumeration value="int" />
            <xs:enumeration value="float" />
            <xs:enumeration value="boolean" />
            <xs:enumeration value="string-select" />
            <xs:enumeration value="string" />
          </xs:restriction>
        </xs:simpleType>
      </xs:attribute>
      <xs:attribute type="xs:string" name="description" use="optional"/>
      <xs:attribute type="xs:string" name="default" use="optional"/>
      <xs:attribute type="xs:string" name="alias" use="optional"/>
      <xs:attribute type="xs:string" name="min" use="optional"/>
      <xs:attribute type="xs:string" name="max" use="optional"/>
    </xs:complexType>
  </xs:element>
  <xs:element name="OptionList">
    <xs:complexType>
      <xs:sequence>
        <xs:element ref="Option" maxOccurs="unbounded" minOccurs="0"/>
      </xs:sequence>
    </xs:complexType>
  </xs:element>
</xs:schema>
""")

    schema = etree.XMLSchema(schema_optionlist)

    xml = gdal.GetTranformerOptionList()
    try:
        parser = etree.XMLParser(schema=schema)
        etree.fromstring(xml, parser)
    except Exception:
        print(xml)
        raise
