Re: AutoIdentifyEPSG() fails on standard ESRI-dialect NAD83/UTM .prj (proj_identify succeeds); FindMatches() also missing from Java bindings
Tom Moore via gdal-dev <[email protected]>
| Newsgroups | gmane.comp.gis.gdal.devel |
|---|---|
| Message-ID | <[email protected]> |
Interesting, eh? It looks like swig generates that. It appears to append argument list position numbers in order to prevent variable name collisions. The trick is that it does the substitution in some places, but not others. I must be missing something fundamental here because it seems to make this more error prone than it should be.
Here is the part of the osr_wrap.cpp file that was generated by swig. You can see how the variable names get assigned.
SWIGEXPORT jobjectArray JNICALL Java_org_gdal_osr_osrJNI_SpatialReference_1FindMatches(JNIEnv *jenv, jclass jcls, jlong jarg1, jobject jarg1_, jobject jarg2, jobjectArray jarg4) {
jobjectArray jresult = 0 ;
OSRSpatialReferenceShadow *arg1 = 0 ;
char **arg2 = 0 ;
int *arg3 = 0 ;
int **arg4 = 0 ;
int nMatches3 = 0 ;
int *panConfidence4 = NULL ;
OSRSpatialReferenceShadow **result = 0 ;
(void)jenv;
(void)jcls;
{
/* %typemap(in,numinputs=0) int* nvalues */
arg3 = &nMatches3;
}
(void)jarg1_;
arg1 = *(OSRSpatialReferenceShadow **)&jarg1;
{
/* %typemap(in) char **options */
arg2 = NULL;
if(jarg2 != 0) {
const jclass vector = jenv->FindClass("java/util/Vector");
const jclass enumeration = jenv->FindClass("java/util/Enumeration");
const jclass stringClass = jenv->FindClass("java/lang/String");
const jmethodID elements = jenv->GetMethodID(vector, "elements",
"()Ljava/util/Enumeration;");
const jmethodID hasMoreElements = jenv->GetMethodID(enumeration,
"hasMoreElements", "()Z");
const jmethodID getNextElement = jenv->GetMethodID(enumeration,
"nextElement", "()Ljava/lang/Object;");
if(vector == NULL || enumeration == NULL || elements == NULL ||
hasMoreElements == NULL || getNextElement == NULL) {
fprintf(stderr, "Could not load (options **) jni types.\n");
return 0;
}
for (jobject keys = jenv->CallObjectMethod(jarg2, elements);
jenv->CallBooleanMethod(keys, hasMoreElements) == JNI_TRUE;) {
jstring value = (jstring)jenv->CallObjectMethod(keys, getNextElement);
if (value == NULL || !jenv->IsInstanceOf(value, stringClass))
{
CSLDestroy(arg2);
SWIG_JavaThrowException(jenv, SWIG_JavaIllegalArgumentException, "an element in the vector is not a string");
return 0;
}
const char *valptr = jenv->GetStringUTFChars(value, 0);
arg2 = CSLAddString(arg2, valptr);
jenv->ReleaseStringUTFChars(value, valptr);
}
}
}
{
/* %typemap(in) int** confidence_values -- caller's array contents are
ignored; we only keep jarg4 around so argout can write the real
result into element [0]. */
arg4 = &panConfidence4;
}
result = (OSRSpatialReferenceShadow **)OSRSpatialReferenceShadow_FindMatches(arg1,arg2,arg3,arg4);
{
/* %typemap(out) (OSRSpatialReferenceShadow**) -- builds the returned SpatialReference[] */
const jclass srsClass = jenv->FindClass("org/gdal/osr/SpatialReference");
const jmethodID srsCtor = jenv->GetMethodID(srsClass, "<init>", "(JZ)V");
jresult = jenv->NewObjectArray(nMatches3, srsClass, NULL);
for (int i = 0; i < nMatches3; i++) {
OSRReference(result[i]);
jobject srsObj = jenv->NewObject(srsClass, srsCtor, (jlong)result[i], (jboolean)true);
jenv->SetObjectArrayElement(jresult, i, srsObj);
jenv->DeleteLocalRef(srsObj);
}
OSRFreeSRSArray(result);
}
{
/* %typemap(argout) int** confidence_values -- fills the caller's int[][] */
if (jarg4 && jenv->GetArrayLength(jarg4) >= 1) {
jintArray confArray = jenv->NewIntArray(nMatches3);
jenv->SetIntArrayRegion(confArray, 0, nMatches3, (jint*)panConfidence4);
jenv->SetObjectArrayElement(jarg4, 0, confArray);
jenv->DeleteLocalRef(confArray);
}
CPLFree(panConfidence4);
}
{
/* %typemap(freearg) char **options */
CSLDestroy( arg2 );
}
return jresult;
}
On Thu, Aug 13, 2026, at 8:28 PM, Barry DeZonia wrote:
> One maybe nonsense question: where does panConfidence4 come from? Are you using an undeclared variable?
>
> On Thu, Aug 13, 2026 at 7:24 PM Barry DeZonia <[email protected]> wrote:
>> My foggy memory tells me that this looks good. I will let you and Even figure out the PR issue.
>>
>> On Thu, Aug 13, 2026 at 6:56 PM Tom Moore <[email protected]> wrote:
>>> __
>>> Thanks for offering to take a look. I have a working draft already, but I'm not sure if I'm taking the right approach and could use a review.
>>>
>>> The signature translation itself is the easy part. The C# binding in osr.i was an almost perfect example, and this is what I changed it to:
>>>
>>> #ifdef SWIGJAVA
>>> OSRSpatialReferenceShadow** FindMatches( char** options, int* nvalues, int** confidence_values )
>>> {
>>> return (OSRSpatialReferenceShadow**) OSRFindMatches(self, options, nvalues, confidence_values);
>>> }
>>> #endif
>>>
>>> Everything interesting (haha, challenging) turned out to be in typemaps_java.i, getting SWIG to actually expose that return value and the two output parameters sensibly in Java. I haven't used swig in a long time, so I had forgotten most of the incantations. Llm to the rescue.
>>>
>>> Here is the addition to typemaps_java.i:
>>> == start ==
>>> %typemap(in,numinputs=0) int* nvalues (int nMatches = 0)
>>> {
>>> /* %typemap(in,numinputs=0) int* nvalues */
>>> $1 = &nMatches;
>>> }
>>>
>>> %typemap(in) int** confidence_values (int* panConfidence = NULL)
>>> {
>>> /* %typemap(in) int** confidence_values. Ignore the input,
>>> keep $input around so argout can write the real
>>> result into element [0]. */
>>> $1 = &panConfidence;
>>> }
>>>
>>> %typemap(argout) int** confidence_values
>>> {
>>> /* %typemap(argout) int** confidence_values will fill the caller's int[][] */
>>> if ($input && jenv->GetArrayLength($input) >= 1) {
>>> jintArray confArray = jenv->NewIntArray(nMatches3);
>>> jenv->SetIntArrayRegion(confArray, 0, nMatches3, (jint*)panConfidence4);
>>> jenv->SetObjectArrayElement($input, 0, confArray);
>>> jenv->DeleteLocalRef(confArray);
>>> }
>>> CPLFree(panConfidence4);
>>> }
>>>
>>> %typemap(jni) int** confidence_values "jobjectArray"
>>> %typemap(jtype) int** confidence_values "int[][]"
>>> %typemap(jstype) int** confidence_values "int[][]"
>>> %typemap(javain) int** confidence_values "$javainput"
>>>
>>> %typemap(out) (OSRSpatialReferenceShadow**)
>>> {
>>> const jclass srsClass = jenv->FindClass("org/gdal/osr/SpatialReference");
>>> const jmethodID srsCtor = jenv->GetMethodID(srsClass, "<init>", "(JZ)V");
>>>
>>> jresult = jenv->NewObjectArray(nMatches3, srsClass, NULL);
>>> for (int i = 0; i < nMatches3; i++) {
>>> OSRReference(result[i]);
>>> jobject srsObj = jenv->NewObject(srsClass, srsCtor, (jlong)result[i], (jboolean)true);
>>> jenv->SetObjectArrayElement(jresult, i, srsObj);
>>> jenv->DeleteLocalRef(srsObj);
>>> }
>>> OSRFreeSRSArray(result);
>>> }
>>>
>>> %typemap(jni) OSRSpatialReferenceShadow** "jobjectArray"
>>> %typemap(jtype) OSRSpatialReferenceShadow** "org.gdal.osr.SpatialReference[]"
>>> %typemap(jstype) OSRSpatialReferenceShadow** "org.gdal.osr.SpatialReference[]"
>>> %typemap(javaout) OSRSpatialReferenceShadow** {
>>> return $jnicall;
>>> }
>>> == end ==
>>>
>>> The nvalues argument stays hidden (numinputs=0), while confidence_values becomes visible as an int[][] out-box, following the same caller-passes-a-1-element-array-and-we-fill-it idiom already used elsewhere in typemaps_java.i (the GDALDimensionHS** pattern was the model here).
>>>
>>> SWIG auto-suffixes typemap-declared local variables with the argument's position in the full parameter list to avoid collisions. FOr example, a variable I declared as (int nMatches = 0) inside the nvalues typemap (argument position 3) actually gets emitted into the generated code as nMatches3, not nMatches. This isn't visible from the .i source at all, I only found it by examining the generated osr_wrap.cpp after a failed compile. Any other typemap block that needs to reference that same variable (the argout/out blocks) has to hardcode the anticipated suffixed name directly because SWIG doesn't rewrite references for you.
>>>
>>> End result:
>>>
>>> public SpatialReference[] FindMatches(Vector options, int[][] confidenceValuesOut)
>>>
>>> The function returns an array of matches (SpatialReference[] as OSRSpatialReferenceShadow**), which is the primary thing that you want to get from this function.
>>>
>>> How to use:
>>> int[][] confidence = new int[1][];
>>> SpatialReference[] matches = srs.FindMatches(null, confidence);
>>> int[] confidenceValues = confidence[0];
>>>
>>> Patched against the current master. Tested on against an esri shapefile .prj (ESRI-dialect NAD83 / UTM zone 11N WKT) that AutoIdentifyEPSG() fails on with 'Unsupported SRS' -- FindMatches() correctly returns a single match, EPSG:26911, with confidence[0][0] = 100, matching projinfo --identify exactly on the same input. Not tested any further than that.
>>>
>>> How does this look so far? Is this going in the right direction? If so, what would it take to turn this into an acceptable PR?
>>>
>>> Thanks again for your help, and full disclosure, thanks to llm for significant assistance on this.
>>> Tom
>>>
>>> On Thu, Aug 13, 2026, at 5:16 PM, Barry DeZonia wrote:
>>>> AI is helping me remember
>>>>
>>>> for the FindMatches() routine would you want it matching as close as possible C types or would you want a simplified API for Java that mogt communicate info via an array of strings (or one formatted string). Sorry if that sounds out of left field.
>>>>
>>>> One AI said this:
>>>> 2. Handle Java JNI Typemaps
>>>> Because `int**` and object arrays (`OSRSpatialReferenceShadow***`) do not map cleanly to standard Java types, you must ensure your SWIG configuration or a companion helper helper translates them into manageable objects on the Java side.
>>>> Alternatively, if dealing with direct pointer manipulation via SWIG typemaps is too complex, developers routinely implement a *custom C++ wrapper function* inside `osr.i` that bundles the results into a string format (such as an array of matching EPSG strings and confidences) before passing it over the JNI boundary:
>>>>
>>>> swig
>>>> `%extend OSRSpatialReferenceShadow {
>>>> // A simplified wrapper returning matches as formatted string arrays to avoid pointer hell
>>>> char** FindMatchesSimple(char** options) {
>>>> int nEntries = 0;
>>>> int* panConfidence = NULL;
>>>> OGRSpatialReferenceH* pahSRS = OGRSpatialReference_FindMatches(self, options, &nEntries, &panConfidence);
>>>>
>>>> char** papszResult = NULL;
>>>> // Construct a structured string array containing "EPSG:Code,Confidence"
>>>> for(int i = 0; i < nEntries; i++) {
>>>> const char* pszAuthName = OGRSpatialReference_GetAttrValue(pahSRS[i], "AUTHORITY", 0);
>>>> const char* pszAuthCode = OGRSpatialReference_GetAttrValue(pahSRS[i], "AUTHORITY", 1);
>>>> // Format string logic, append to papszResult...
>>>> // BDZ - TODO - NOTE - This is the real work needed to finish this method.
>>>> }
>>>>
>>>> OSRFreeSRSArray(pahSRS);
>>>> CPLFree(panConfidence);
>>>> return papszResult;
>>>> }
>>>> }`
>>>>
>>>> On Thu, Aug 13, 2026 at 3:08 PM Barry DeZonia <[email protected]> wrote:
>>>>> I could maybe help here but, good god, it's been a long time since I've worked on the Java bindings and I remember almost nothing. Maybe if I inspect the Java bindings code I will remember some stuff. Naively, the function signature looks pretty simple to translate.
>>>>>
>>>>> On Thu, Aug 13, 2026 at 6:53 AM Even Rouault via gdal-dev <[email protected]> wrote:
>>>>>> __
>>>>>> Tom,
>>>>>>
>>>>>> yes FindMatches() is what is needed for your use case and it is not currently available in the Java bindings. That would require someone to write the appropriate typemap(s) to map the C types to Java types.
>>>>>>
>>>>>> Even
>>>>>>
>>>>>> Le 13/08/2026 à 13:15, Tom Moore via gdal-dev a écrit :
>>>>>>> Hi all,
>>>>>>>
>>>>>>> I'm running into a case where SpatialReference.AutoIdentifyEPSG() fails on a completely standard, well-formed ESRI-dialect .prj file, even though the underlying PROJ identification machinery (proj_identify(), as exercised via "projinfo --identify") resolves it correctly with 100% confidence. I'd like to know whether this is a known limitation, and whether there's a recommended workaround for Java bindings users specifically, since FindMatches() isn't exposed there.
>>>>>>>
>>>>>>> Environment:
>>>>>>> - GDAL 3.11.1 (Windows build)
>>>>>>> - PROJ 9.6.2
>>>>>>> - Java bindings (org.gdal.osr / SWIG)
>>>>>>>
>>>>>>> Input WKT (contents of a shapefile .prj, ESRI dialect):
>>>>>>>
>>>>>>> PROJCS["NAD_1983_UTM_Zone_11N",GEOGCS["GCS_North_American_1983",DATUM["D_North_American_1983",SPHEROID["GRS_1980",6378137.0,298.257222101]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["False_Easting",500000.0],PARAMETER["False_Northing",0.0],PARAMETER["Central_Meridian",-117.0],PARAMETER["Scale_Factor",0.9996],PARAMETER["Latitude_Of_Origin",0.0],UNIT["Meter",1.0]]
>>>>>>>
>>>>>>> QGIS correctly identifies this as EPSG:26911.
>>>>>>>
>>>>>>> What I tried (Java):
>>>>>>>
>>>>>>> SpatialReference srs = new SpatialReference();
>>>>>>> srs.ImportFromESRI(lines); // also tried plain ImportFromWkt() but got the same result
>>>>>>> srs.SetAxisMappingStrategy(osrConstants.OAMS_TRADITIONAL_GIS_ORDER);
>>>>>>>
>>>>>>> try {
>>>>>>> srs.AutoIdentifyEPSG();
>>>>>>> } catch (Exception ex) {
>>>>>>> System.out.println("Exception: " + ex.getMessage());
>>>>>>> }
>>>>>>> System.out.println("code = " + srs.GetAuthorityCode(null));
>>>>>>>
>>>>>>> Result:
>>>>>>>
>>>>>>> Exception: OGR Error: Unsupported SRS
>>>>>>> code = null
>>>>>>>
>>>>>>> gdal.GetLastErrorType() / GetLastErrorMsg() are empty. No additional diagnostic detail is surfaced beyond the generic exception.
>>>>>>>
>>>>>>> I ruled out several things before suspecting this is a AutoIdentifyEPSG() limitation rather than an environment/config issue on my end:
>>>>>>>
>>>>>>> 1. proj.db itself is correct. Confirmed by mounting the exact same proj.db (from the PROJ 9 data directory used by my JAva app) into a clean ghcr.io/osgeo/gdal:ubuntu-small-latest container and running `gdalsrsinfo -e` against the same .prj file. It resolved to EPSG:26911 correctly, full WKT2 with all authority IDs.
>>>>>>> 2. PROJ_LIB / PROJ_DATA environment variables are set correctly and confirmed using System.getenv() to be visible to the process
>>>>>>> 3. Tried MorphFromESRI() (both via ImportFromESRI() alone, and combined with an explicit MorphFromESRI() call) - no change.
>>>>>>> 4. Tried explicit SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER) before calling AutoIdentifyEPSG() - no change.
>>>>>>> 5. Ran the projinfo.exe from the gdal build that I am using with the Java app directly against the same .prj file:
>>>>>>>
>>>>>>> projinfo.exe --identify fmu.prj
>>>>>>> ...
>>>>>>> Identification match count: 1
>>>>>>> EPSG:26911: 100 %
>>>>>>>
>>>>>>> So proj_identify() / FindMatches()-equivalent logic resolves this WKT perfectly in the exact same native build, but AutoIdentifyEPSG() fails on it.
>>>>>>>
>>>>>>> This looks consistent with a few things I found in the tracker/mailing list archives while investigating:
>>>>>>>
>>>>>>> - https://trac.osgeo.org/gdal/ticket/6188 -- maintainer comment noting AutoIdentifyEPSG() has "very limited capabilities" and would need fuzzy matching to handle inexact datum/ellipsoid names, "especially when dealing with WKT coming from ESRI."
>>>>>>> - https://github.com/OSGeo/gdal/issues/4038 -- near-identical repro WKT (ESRI-dialect UTM), with a maintainer noting AutoIdentifyEPSG() can return OGRERR_UNSUPPORTED_SRS while having still injected partial AUTHORITY tags into sub-nodes. This matches what I see when printing the WKT after the failed call (DATUM gets EPSG:6269, but the top-level PROJCS never gets an ID).
>>>>>>> - https://github.com/OSGeo/gdal/issues/2303 -- shows the standard Python workaround (fall back to FindMatches() when AutoIdentifyEPSG() fails), which leads to my second question below.
>>>>>>>
>>>>>>> Questions:
>>>>>>>
>>>>>>> 1. Is AutoIdentifyEPSG() failing on this ESRI-dialect NAD83/UTM WKT a known limitation, or does this look like a genuine regression/bug worth filing?
>>>>>>> 2. FindMatches() does not appear to be exposed in the Java SWIG bindings (org.gdal.osr.SpatialReference). Is that correct, or am I missing something? If it's genuinely absent, is there any appetite for adding it, given it's the documented fallback for exactly this situation in other language bindings?
>>>>>>> 3. Short of shelling out to "projinfo --identify" as a subprocess, is there a recommended in-process approach for Java bindings users to reach the same identification logic?
>>>>>>>
>>>>>>> Happy to provide a minimal reproducible test case / file a tracker issue if that's useful, but I wanted to check here first in case this is already understood behavior.
>>>>>>>
>>>>>>> Thanks,
>>>>>>> Tom
>>>>>>>
>>>>>>>
>>>>>>> _______________________________________________
>>>>>>> gdal-dev mailing list
>>>>>>> [email protected]
>>>>>>> https://lists.osgeo.org/mailman/listinfo/gdal-dev
>>>>>> --
>>>>>> http://www.spatialys.com
>>>>>> My software is free, but my time generally not.
>>>>>> LLMs contribute to global warming and brain rot.
>>>>>> Let's guillotine them! "Ah ! ça ira, ça ira, ça ira !"
>>>>>> _______________________________________________
>>>>>> gdal-dev mailing list
>>>>>> [email protected]
>>>>>> https://lists.osgeo.org/mailman/listinfo/gdal-dev
>>>
--
Tom Moore
Spatial Planning Systems
960 Burkes Bluff Lane
Deep River ON K0J 1P0
Canada
Phone: +1 613 584 9354
_______________________________________________
gdal-dev mailing list
[email protected]
https://lists.osgeo.org/mailman/listinfo/gdal-dev