This commit is contained in:
nq
2026-10-14 11:16:43 +00:00
parent ac550e327d
commit dfaa54ed8e
119 changed files with 8860 additions and 0 deletions
BIN
View File
Binary file not shown.
+56
View File
@@ -0,0 +1,56 @@
"""Generate a tiny unlocked classic Garmin IMG with one RGN vector feature.
Only for exercising the converter's control flow, NOT a real-world compatibility test.
"""
from pathlib import Path
import struct
import sys
def u24(n): return (n & 0xffffff).to_bytes(3,'little')
def coord(deg):return round(deg*(1<<24)/360)
def bitstream(values):
bits = [1,0,1,0] # x same-sign+, y same-sign+
for x,y in values:
for value in (x,y):
bits += [(value >> i)&1 for i in range(11)]
return bytes(sum(bits[i+j]<<j for j in range(min(8,len(bits)-i))) for i in range(0,len(bits),8))
def gen(dest):
blocksize=512
blob=bytearray(512*20)
blob[0x10:0x17]=b'DSKIMG\0'
blob[0x41:0x48]=b'GARMIN\0'
blob[0x61]=9;blob[0x62]=0
blob[0x40c:0x410]=(512*8).to_bytes(4,'little')
for j,(ext,block,length) in enumerate([('TRE',8,0xe0),('RGN',9,0xd0)]):
at=0x600+j*512
blob[at]=1;blob[at+1:at+9]=b'TESTMAP '
blob[at+9:at+12]=ext.encode()
struct.pack_into('<IHH',blob,at+12,length,0,0)
struct.pack_into('<H',blob,at+32,block)
struct.pack_into('<H',blob,at+34,0xffff)
tre=bytearray(0xe0)
struct.pack_into('<H',tre,0,0xc0)
tre[2:12]=b'GARMIN TRE'
for at,value in [(0x15,42.35),(0x18,25.05),(0x1b,41.85),(0x1e,24.45)]:
tre[at:at+3]=u24(coord(value))
struct.pack_into('<IIII',tre,0x21,0xc0,4,0xc4,14)
# level with bits=20 and 1 subdivision
tre[0xc0:0xc4]=bytes([0,20,1,0])
sub=bytearray(14)
sub[0:3]=u24(0);sub[3]=0x40
sub[4:7]=u24(coord(24.75))
sub[7:10]=u24(coord(42.1))
struct.pack_into('<HH',sub,10,1200,1200)
tre[0xc4:0xd2]=sub
blob[8*512:8*512+len(tre)]=tre
rgn=bytearray(0xd0)
struct.pack_into('<H',rgn,0,0x7d)
rgn[2:12]=b'GARMIN RGN'
path=bitstream([(2000,2000)]*4)
geom=bytes([0x03,0,0,0])+struct.pack('<hh',0,0)+bytes([len(path),0x99])+path
struct.pack_into('<II',rgn,0x15,0x7d,len(geom))
rgn[0x7d:0x7d+len(geom)]=geom
blob[9*512:9*512+len(rgn)]=rgn
Path(dest).write_bytes(blob)
if __name__=='__main__':gen(sys.argv[1])
+88
View File
@@ -0,0 +1,88 @@
"""Synthetic native IMG regression tests; does not assert real Garmin compatibility."""
import sqlite3
import struct
import subprocess
import sys
import tempfile
from pathlib import Path
from make_fixture import gen,coord,u24
binary=Path(sys.argv[1]); work=Path(tempfile.mkdtemp(prefix='garmin2osmand-'))
try:
classic=work/'classic.img';gen(classic)
gmp=work/'gmp.img'
original=classic.read_bytes()
b=bytearray(len(original))
b[0:0x600]=original[:0x600]
# Garmin FAT record for one GMP file, backed by four 512-byte blocks.
b[0x600]=1;b[0x601:0x609]=b'TESTMAP '
b[0x609:0x60c]=b'GMP'
struct.pack_into('<IH',b,0x60c,2048,0)
for k in range(4):struct.pack_into('<H',b,0x620+2*k,8+k)
struct.pack_into('<H',b,0x628,0xffff)
g=bytearray(2048)
struct.pack_into('<H',g,0,0x35);g[2:12]=b'GARMIN GMP'
struct.pack_into('<II',g,0x19,0x100,0x400)
tre=bytearray(original[8*512:9*512]);rgn=bytearray(original[9*512:10*512])
struct.pack_into('<II',tre,0x21,0x1c0,4)
struct.pack_into('<II',tre,0x29,0x1c4,14)
struct.pack_into('<II',rgn,0x15,0x47d,struct.unpack_from('<I',rgn,0x19)[0])
g[0x100:0x100+0xd2]=tre[:0xd2]
g[0x400:0x400+0x8b]=rgn[:0x8b]
# RGN1 data has to live at its new absolute GMP offset.
g[0x47d:0x47d+20]=rgn[0x7d:0x7d+20]
b[8*512:12*512]=g
gmp.write_bytes(b)
for kind,img in [('classic',classic),('gmp',gmp)]:
info=subprocess.run([binary,'--input',img,'--info'],capture_output=True,text=True)
assert info.returncode==0,(kind,info.stderr)
out=work/f'{kind}.sqlitedb'
cmd=[binary,'--input',img,'--output',out,'--bbox','24.5,41.85,25.05,42.35','--min-zoom','12','--max-zoom','12']
p=subprocess.run(cmd,capture_output=True,text=True)
assert p.returncode==0,(kind,p.stdout,p.stderr)
db=sqlite3.connect(out)
count=db.execute('select count(*) from tiles').fetchone()[0]
assert count>0,kind
assert db.execute('pragma integrity_check').fetchone()[0]=='ok'
assert db.execute('select hex(substr(image,1,8)) from tiles limit 1').fetchone()[0]=='89504E470D0A1A0A'
assert db.execute('select tilenumbering from info').fetchone()[0]=='simple'
db.close()
resumed=subprocess.run(cmd+['--resume'],capture_output=True,text=True)
assert resumed.returncode==0,(kind,resumed.stderr)
capped=subprocess.run(cmd[:-0]+['--max-tiles','1'] if False else
[binary,'--input',img,'--output',work/f'{kind}-cap.sqlitedb','--max-tiles','1'],capture_output=True,text=True)
assert capped.returncode!=0 and not (work/f'{kind}-cap.sqlitedb').exists()
# Plan mode must not create a DB, and --whole-map bypasses 100k budget automatically.
plan=subprocess.run([binary,'--input',img,'--whole-map','--min-zoom','12','--max-zoom','12','--plan'],capture_output=True,text=True)
assert plan.returncode==0,(kind,plan.stdout,plan.stderr)
assert 'Tile candidates:' in plan.stdout
whole=work/f'{kind}-whole.sqlitedb'
full_cmd=[binary,'--input',img,'--output',whole,'--whole-map','--min-zoom','12','--max-zoom','12']
p=subprocess.run(full_cmd,capture_output=True,text=True)
assert p.returncode==0,(kind,p.stdout,p.stderr)
with sqlite3.connect(whole) as db:
assert db.execute('pragma integrity_check').fetchone()[0]=='ok'
assert db.execute('select count(*) from tiles').fetchone()[0]>0
assert db.execute('select count(*) from progress').fetchone()[0]>0
assert db.execute('select count(*) from converter_settings').fetchone()[0]==1
original=db.execute('select count(*) from tiles').fetchone()[0]
resumed=subprocess.run(full_cmd+['--resume'],capture_output=True,text=True)
assert resumed.returncode==0 and 'candidate tiles visited=0' in resumed.stdout,(kind,resumed.stdout,resumed.stderr)
with sqlite3.connect(whole) as db:
assert db.execute('select count(*) from tiles').fetchone()[0]==original
changed=subprocess.run(full_cmd+['--resume','--aa','1'],capture_output=True,text=True)
assert changed.returncode!=0 and 'Resume settings do not match' in changed.stderr
# Force a small byte budget to exercise persistent scanline checkpoints.
limited=work/f'{kind}-limited.sqlitedb'
lim_cmd=[binary,'--input',img,'--output',limited,'--whole-map','--min-zoom','12','--max-zoom','12']
failrun=subprocess.run(lim_cmd+['--max-bytes','1'],capture_output=True,text=True)
assert failrun.returncode!=0 and limited.is_file()
recovery=subprocess.run(lim_cmd+['--max-bytes','100000000','--resume'],capture_output=True,text=True)
assert recovery.returncode==0,(kind,recovery.stdout,recovery.stderr)
with sqlite3.connect(limited) as db:
assert db.execute('pragma integrity_check').fetchone()[0]=='ok'
assert db.execute('select count(*) from tiles').fetchone()[0]==original
print(kind, 'PASSED tiles',count,'plus whole-map, checkpoint, restart and settings validation')
finally:
import shutil
shutil.rmtree(work)
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env python3
"""Independent, streaming validation of the standalone C++ IMG->OSM pipeline."""
import subprocess
import sys
import tempfile
import xml.etree.ElementTree as ET
from pathlib import Path
binary = Path(sys.argv[1]).resolve()
fixture = Path(__file__).with_name("demo.img")
with tempfile.TemporaryDirectory() as tmp:
destination = Path(tmp) / "smoke.osm"
result = subprocess.run([str(binary), '--input', str(fixture), '--output', str(destination)], capture_output=True, text=True)
assert result.returncode == 0, (result.stdout, result.stderr)
ids, ways, referenced = set(), 0, []
for _event, e in ET.iterparse(destination, events=('end',)):
if e.tag == 'node':
assert e.attrib['id'] not in ids
ids.add(e.attrib['id'])
elif e.tag == 'way':
ways += 1
refs = [n.attrib['ref'] for n in e.findall('nd')]
assert len(refs) >= 2
referenced.extend(refs)
assert any(t.attrib.get('k') == 'highway' for t in e.findall('tag'))
if e.tag in ('node', 'way'):
e.clear()
assert ways == 1, ways
assert all(n in ids for n in referenced)
assert len(ids) >= 2
print('Standalone Garmin IMG -> OSM XML fixture test passed')
+18
View File
@@ -0,0 +1,18 @@
"""Verify an OsmAnd simple XYZ SQLite raster map with Python stdlib only."""
import sqlite3
import struct
import sys
path = sys.argv[1]
db = sqlite3.connect(f'file:{path}?mode=ro', uri=True)
assert db.execute('PRAGMA integrity_check').fetchone()[0] == 'ok'
info = db.execute('SELECT tilenumbering,minzoom,maxzoom,tilesize FROM info').fetchone()
assert info[0] == 'simple' and info[3] == 256, info
zlo, zhi = int(info[1]), int(info[2])
counts = db.execute('SELECT z, COUNT(*) FROM tiles GROUP BY z ORDER BY z').fetchall()
assert counts and min(z for z,n in counts)>=zlo and max(z for z,n in counts)<=zhi
for x,y,z,img in db.execute('SELECT x,y,z,image FROM tiles'):
assert 0<=x<1<<z and 0<=y<1<<z and zlo<=z<=zhi, (x,y,z)
assert img[:8]==b'\x89PNG\r\n\x1a\n'
assert struct.unpack('>II',img[16:24])==(256,256)
print('OK:',path, 'metadata=',info, 'tiles=',counts)