Coverage for gws-app/gws/lib/crs/__init__.py: 90%

321 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-24 12:46 +0200

1from typing import Optional 

2 

3import math 

4import re 

5import warnings 

6 

7import pyproj.crs 

8import pyproj.exceptions 

9import pyproj.transformer 

10 

11import gws 

12 

13 

14## 

15 

16 

17class Object(gws.Crs): 

18 def __init__(self, **kwargs): 

19 vars(self).update(kwargs) 

20 

21 # crs objects with the same srid must be equal 

22 # (despite caching, they can be different due to pickling) 

23 

24 def __hash__(self): 

25 return self.srid 

26 

27 def __eq__(self, other): 

28 return isinstance(other, Object) and other.srid == self.srid 

29 

30 def __repr__(self): 

31 return f'<crs:{self.srid}>' 

32 

33 def axis_for_format(self, fmt): 

34 if not self.isYX: 

35 return self.axis 

36 return _AXIS_FOR_FORMAT.get(fmt, self.axis) 

37 

38 def transform_extent(self, ext, crs_to): 

39 if crs_to == self: 

40 return ext 

41 return _transform_extent_check(ext, self.srid, crs_to.srid) 

42 

43 def transformer(self, crs_to): 

44 tr = _pyproj_transformer(self.srid, crs_to.srid) 

45 return tr.transform 

46 

47 def extent_size_in_meters(self, extent): 

48 x0, y0, x1, y1 = extent 

49 

50 if self.isProjected: 

51 if self.uom != gws.Uom.m: 

52 # @TODO support non-meter crs 

53 raise Error(f'unsupported unit: {self.uom}') 

54 return abs(x1 - x0), abs(y1 - y0) 

55 

56 geod = pyproj.Geod(ellps='WGS84') 

57 

58 mid_lat = (y0 + y1) / 2 

59 _, _, w = geod.inv(x0, mid_lat, x1, mid_lat) 

60 mid_lon = (x0 + x1) / 2 

61 _, _, h = geod.inv(mid_lon, y0, mid_lon, y1) 

62 

63 return w, h 

64 

65 def point_offset_in_meters(self, xy, dist, az): 

66 x, y = xy 

67 

68 if self.isProjected: 

69 if self.uom != gws.Uom.m: 

70 # @TODO support non-meter crs 

71 raise Error(f'unsupported unit: {self.uom}') 

72 

73 if az == 0: 

74 return x, y + dist 

75 if az == 90: 

76 return x + dist, y 

77 if az == 180: 

78 return x, y - dist 

79 if az == 270: 

80 return x - dist, y 

81 

82 az_rad = math.radians(90 - az) 

83 return ( 

84 x + dist * math.cos(az_rad), 

85 y + dist * math.sin(az_rad), 

86 ) 

87 

88 geod = pyproj.Geod(ellps='WGS84') 

89 x, y, _ = geod.fwd(x, y, dist=dist, az=az) 

90 return x, y 

91 

92 def to_string(self, fmt=None): 

93 fmt = fmt or gws.CrsFormat.epsg 

94 if fmt == gws.CrsFormat.srid: 

95 return str(self.srid) 

96 return getattr(self, str(fmt).lower()) 

97 

98 def to_geojson(self): 

99 # https://geojson.org/geojson-spec#named-crs 

100 return { 

101 'type': 'name', 

102 'properties': { 

103 'name': self.urn, 

104 }, 

105 } 

106 

107 

108# 

109 

110 

111def qgis_extent_width(extent: gws.Extent) -> float: 

112 # straight port from QGIS/src/core/qgsscalecalculator.cpp QgsScaleCalculator::calculateGeographicDistance 

113 x0, y0, x1, y1 = extent 

114 

115 lat = (y0 + y1) * 0.5 

116 RADS = (4.0 * math.atan(1.0)) / 180.0 

117 a = math.pow(math.cos(lat * RADS), 2) 

118 c = 2.0 * math.atan2(math.sqrt(a), math.sqrt(1.0 - a)) 

119 RA = 6378000 

120 E = 0.0810820288 

121 radius = RA * (1.0 - E * E) / math.pow(1.0 - E * E * math.sin(lat * RADS) * math.sin(lat * RADS), 1.5) 

122 return (x1 - x0) / 180.0 * radius * c 

123 

124 

125# 

126 

127# enough precision to represent 1cm 

128COORDINATE_PRECISION_DEG = 7 

129COORDINATE_PRECISION_M = 2 

130 

131WGS84: gws.Crs = Object( 

132 srid=4326, 

133 proj4text='+proj=longlat +datum=WGS84 +no_defs +type=crs', 

134 wkt='GEOGCRS["WGS 84",ENSEMBLE["World Geodetic System 1984 ensemble",MEMBER["World Geodetic System 1984 (Transit)"],MEMBER["World Geodetic System 1984 (G730)"],MEMBER["World Geodetic System 1984 (G873)"],MEMBER["World Geodetic System 1984 (G1150)"],MEMBER["World Geodetic System 1984 (G1674)"],MEMBER["World Geodetic System 1984 (G1762)"],MEMBER["World Geodetic System 1984 (G2139)"],ELLIPSOID["WGS 84",6378137,298.257223563,LENGTHUNIT["metre",1]],ENSEMBLEACCURACY[2.0]],PRIMEM["Greenwich",0,ANGLEUNIT["degree",0.0174532925199433]],CS[ellipsoidal,2],AXIS["geodetic latitude (Lat)",north,ORDER[1],ANGLEUNIT["degree",0.0174532925199433]],AXIS["geodetic longitude (Lon)",east,ORDER[2],ANGLEUNIT["degree",0.0174532925199433]],USAGE[SCOPE["Horizontal component of 3D system."],AREA["World."],BBOX[-90,-180,90,180]],ID["EPSG",4326]]', 

135 axis=gws.Axis.yx, 

136 uom=gws.Uom.deg, 

137 isGeographic=True, 

138 isProjected=False, 

139 isYX=True, 

140 epsg='EPSG:4326', 

141 urn='urn:ogc:def:crs:EPSG::4326', 

142 urnx='urn:x-ogc:def:crs:EPSG:4326', 

143 url='http://www.opengis.net/gml/srs/epsg.xml#4326', 

144 uri='http://www.opengis.net/def/crs/epsg/0/4326', 

145 name='WGS 84', 

146 base=0, 

147 datum='World Geodetic System 1984 ensemble', 

148 wgsExtent=(-180, -90, 180, 90), 

149 extent=(-180, -90, 180, 90), 

150 coordinatePrecision=COORDINATE_PRECISION_DEG, 

151) 

152 

153WGS84.bounds = gws.Bounds(crs=WGS84, extent=WGS84.extent) 

154 

155WEBMERCATOR: gws.Crs = Object( 

156 srid=3857, 

157 proj4text='+proj=merc +a=6378137 +b=6378137 +lat_ts=0 +lon_0=0 +x_0=0 +y_0=0 +k=1 +units=m +nadgrids=@null +wktext +no_defs +type=crs', 

158 wkt='PROJCRS["WGS 84 / Pseudo-Mercator",BASEGEOGCRS["WGS 84",ENSEMBLE["World Geodetic System 1984 ensemble",MEMBER["World Geodetic System 1984 (Transit)"],MEMBER["World Geodetic System 1984 (G730)"],MEMBER["World Geodetic System 1984 (G873)"],MEMBER["World Geodetic System 1984 (G1150)"],MEMBER["World Geodetic System 1984 (G1674)"],MEMBER["World Geodetic System 1984 (G1762)"],MEMBER["World Geodetic System 1984 (G2139)"],ELLIPSOID["WGS 84",6378137,298.257223563,LENGTHUNIT["metre",1]],ENSEMBLEACCURACY[2.0]],PRIMEM["Greenwich",0,ANGLEUNIT["degree",0.0174532925199433]],ID["EPSG",4326]],CONVERSION["Popular Visualisation Pseudo-Mercator",METHOD["Popular Visualisation Pseudo Mercator",ID["EPSG",1024]],PARAMETER["Latitude of natural origin",0,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8801]],PARAMETER["Longitude of natural origin",0,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8802]],PARAMETER["False easting",0,LENGTHUNIT["metre",1],ID["EPSG",8806]],PARAMETER["False northing",0,LENGTHUNIT["metre",1],ID["EPSG",8807]]],CS[Cartesian,2],AXIS["easting (X)",east,ORDER[1],LENGTHUNIT["metre",1]],AXIS["northing (Y)",north,ORDER[2],LENGTHUNIT["metre",1]],USAGE[SCOPE["Web mapping and visualisation."],AREA["World between 85.06°S and 85.06°N."],BBOX[-85.06,-180,85.06,180]],ID["EPSG",3857]]', 

159 axis=gws.Axis.xy, 

160 uom=gws.Uom.m, 

161 isGeographic=False, 

162 isProjected=True, 

163 isYX=False, 

164 epsg='EPSG:3857', 

165 urn='urn:ogc:def:crs:EPSG::3857', 

166 urnx='urn:x-ogc:def:crs:EPSG:3857', 

167 url='http://www.opengis.net/gml/srs/epsg.xml#3857', 

168 uri='http://www.opengis.net/def/crs/epsg/0/3857', 

169 name='WGS 84 / Pseudo-Mercator', 

170 base=4326, 

171 datum='World Geodetic System 1984 ensemble', 

172 wgsExtent=(-180, -85.06, 180, 85.06), 

173 extent=( 

174 -20037508.342789244, 

175 -20048966.104014598, 

176 20037508.342789244, 

177 20048966.104014598, 

178 ), 

179 coordinatePrecision=COORDINATE_PRECISION_M, 

180) 

181 

182WEBMERCATOR.bounds = gws.Bounds(crs=WEBMERCATOR, extent=WEBMERCATOR.extent) 

183 

184WEBMERCATOR_RADIUS = 6378137 

185WEBMERCATOR_SQUARE = ( 

186 -math.pi * WEBMERCATOR_RADIUS, 

187 -math.pi * WEBMERCATOR_RADIUS, 

188 +math.pi * WEBMERCATOR_RADIUS, 

189 +math.pi * WEBMERCATOR_RADIUS, 

190) 

191 

192 

193class Error(gws.Error): 

194 pass 

195 

196 

197def get(crs_name: Optional[gws.CrsName]) -> Optional[gws.Crs]: 

198 """Returns the CRS for a given CRS-code or SRID.""" 

199 if not crs_name: 

200 return None 

201 return _get_crs(crs_name) 

202 

203 

204def parse(crs_name: gws.CrsName) -> tuple[gws.CrsFormat, Optional[gws.Crs]]: 

205 """Parses a CRS to a tuple of CRS-format and the CRS itself.""" 

206 fmt, srid = _parse(crs_name) 

207 if not fmt: 

208 return gws.CrsFormat.none, None 

209 return fmt, _get_crs(srid) 

210 

211 

212def require(crs_name: gws.CrsName) -> gws.Crs: 

213 """Raises an error if no correct CRS is given.""" 

214 crs = _get_crs(crs_name) 

215 if not crs: 

216 raise Error(f'invalid CRS {crs_name!r}') 

217 return crs 

218 

219 

220## 

221 

222 

223def best_match(crs: gws.Crs, supported_crs: list[gws.Crs]) -> gws.Crs: 

224 """Return a crs from the list that most closely matches the given crs. 

225 

226 Args: 

227 crs: target CRS 

228 supported_crs: CRS list 

229 

230 Returns: 

231 A CRS object 

232 """ 

233 

234 if crs in supported_crs: 

235 return crs 

236 

237 bst = _best_match(crs, supported_crs) 

238 if not bst: 

239 bst = supported_crs[0] if supported_crs else crs 

240 gws.log.debug(f'CRS: best_crs: using {bst.srid!r} for {crs.srid!r}') 

241 return bst 

242 

243 

244def _best_match(crs, supported_crs): 

245 # @TODO find a projection with less errors 

246 # @TODO find a projection with same units 

247 

248 if crs.isProjected: 

249 # for a projected crs, find webmercator 

250 for sup in supported_crs: 

251 if sup.srid == WEBMERCATOR.srid: 

252 return sup 

253 

254 # not found, return the first projected crs 

255 for sup in supported_crs: 

256 if sup.isProjected: 

257 return sup 

258 

259 if crs.isGeographic: 

260 # for a geographic crs, try wgs first 

261 for sup in supported_crs: 

262 if sup.srid == WGS84.srid: 

263 return sup 

264 

265 # not found, return the first geographic crs 

266 for sup in supported_crs: 

267 if sup.isGeographic: 

268 return sup 

269 

270 

271## 

272 

273 

274def _get_crs(crs_name): 

275 if crs_name in _obj_cache: 

276 return _obj_cache[crs_name] 

277 

278 fmt, srid = _parse(crs_name) 

279 if not fmt: 

280 gws.log.warning(f'CRS: cannot parse {crs_name!r}') 

281 _obj_cache[crs_name] = None 

282 return None 

283 

284 if srid in _obj_cache: 

285 _obj_cache[crs_name] = _obj_cache[srid] 

286 return _obj_cache[srid] 

287 

288 obj = _get_new_crs(srid) 

289 _obj_cache[crs_name] = _obj_cache[srid] = obj 

290 return obj 

291 

292 

293def _get_new_crs(srid): 

294 pp = _pyproj_crs_object(srid) 

295 if not pp: 

296 gws.log.warning(f'CRS: unknown srid {srid!r}') 

297 return None 

298 

299 au = _axis_and_unit(pp) 

300 if not au: 

301 gws.log.warning(f'CRS: unsupported srid {srid!r}') 

302 return None 

303 

304 axis, uom = au 

305 if uom not in (gws.Uom.m, gws.Uom.deg): 

306 gws.log.warning(f'CRS: unsupported unit {uom!r} for {srid!r}') 

307 return None 

308 

309 return _make_crs(srid, pp, axis, uom) 

310 

311 

312def _pyproj_crs_object(srid) -> Optional[pyproj.CRS]: 

313 if srid in _pyproj_cache: 

314 return _pyproj_cache[srid] 

315 

316 try: 

317 pp = pyproj.CRS.from_epsg(srid) 

318 except pyproj.exceptions.CRSError: 

319 return None 

320 

321 _pyproj_cache[srid] = pp 

322 return _pyproj_cache[srid] 

323 

324 

325def _pyproj_transformer(srid_from, srid_to) -> pyproj.transformer.Transformer: 

326 key = srid_from, srid_to 

327 

328 if key in _transformer_cache: 

329 return _transformer_cache[key] 

330 

331 pa = _pyproj_crs_object(srid_from) 

332 pb = _pyproj_crs_object(srid_to) 

333 

334 _transformer_cache[key] = pyproj.transformer.Transformer.from_crs(pa, pb, always_xy=True) 

335 return _transformer_cache[key] 

336 

337 

338def _transform_extent_check(ext, srid_from, srid_to): 

339 ext_nor = _normalize_extent(ext) 

340 

341 if srid_from == WGS84.srid: 

342 ext_wgs = ext_nor 

343 else: 

344 tr_to_wgs = _pyproj_transformer(srid_from, WGS84.srid) 

345 ext_wgs = tr_to_wgs.transform_bounds(ext_nor[0], ext_nor[1], ext_nor[2], ext_nor[3]) 

346 

347 if srid_to == WGS84.srid: 

348 return _normalize_extent(ext_wgs) 

349 

350 pp = _pyproj_crs_object(srid_to) 

351 if not pp: 

352 raise Error(f'_transform_extent: unknown {srid_to=}') 

353 

354 tr_from_wgs = _pyproj_transformer(WGS84.srid, srid_to) 

355 au = pp.area_of_use 

356 

357 if au: 

358 ext_au = au.bounds 

359 if _is_big_extent(ext_wgs) and not _is_big_extent(ext_au): 

360 ext_to = _transform_extent_sampled(ext_wgs, tr_from_wgs, au) 

361 if ext_to: 

362 gws.log.debug(f'transform_extent: {ext=} {srid_from!r}->{srid_to!r}: big extent {ext_to=} ') 

363 return _normalize_extent(ext_to) 

364 

365 if ext_wgs[2] < ext_au[0] or ext_wgs[0] > ext_au[2] or ext_wgs[3] < ext_au[1] or ext_wgs[1] > ext_au[3]: 

366 gws.log.warning(f'transform_extent: {ext=} {srid_from!r}->{srid_to!r}: outside of AoU ') 

367 

368 ext_to = tr_from_wgs.transform_bounds(ext_wgs[0], ext_wgs[1], ext_wgs[2], ext_wgs[3]) 

369 

370 return _normalize_extent(ext_to) 

371 

372 

373def _transform_extent_sampled(ext_wgs, tr, au, samples=50): 

374 x0, y0, x1, y1 = ext_wgs 

375 ax0, ay0, ax1, ay1 = au.bounds 

376 mid_lon = (ax0 + ax1) / 2 

377 

378 xys = [] 

379 

380 # 1) Dense grid sampling within the area of use (accurate core extent) 

381 for i in range(samples + 1): 

382 lon = ax0 + (ax1 - ax0) * i / samples 

383 for j in range(samples + 1): 

384 lat = ay0 + (ay1 - ay0) * j / samples 

385 try: 

386 xys.append(tr.transform(lon, lat, errcheck=True)) 

387 except Exception: 

388 pass 

389 

390 # 2) Sample the full latitude range along the central meridian of the AoU 

391 # This captures the full Y extent for "the world" in the target projection 

392 for i in range(samples + 1): 

393 lat = y0 + (y1 - y0) * i / samples 

394 try: 

395 xys.append(tr.transform(mid_lon, lat, errcheck=True)) 

396 except Exception: 

397 pass 

398 

399 # 3) Sample several meridians across the full longitude range 

400 # to capture the full X spread at various latitudes 

401 for i in range(samples + 1): 

402 lon = x0 + (x1 - x0) * i / samples 

403 for j in range(samples + 1): 

404 lat = y0 + (y1 - y0) * j / samples 

405 try: 

406 xys.append(tr.transform(lon, lat, errcheck=True)) 

407 except Exception: 

408 pass 

409 

410 xs = [x for x, y in xys if math.isfinite(x) and math.isfinite(y)] 

411 ys = [y for x, y in xys if math.isfinite(x) and math.isfinite(y)] 

412 

413 if not xs: 

414 return 

415 

416 return (min(xs), min(ys), max(xs), max(ys)) 

417 

418 

419def _is_big_extent(ext_wgs): 

420 dx = abs(ext_wgs[2] - ext_wgs[0]) 

421 dy = abs(ext_wgs[3] - ext_wgs[1]) 

422 return dx > 350 or dy > 160 

423 

424 

425def _transform_extent_direct(ext, srid_from, srid_to): 

426 tr = _pyproj_transformer(srid_from, srid_to) 

427 

428 ext_nor = _normalize_extent(ext) 

429 

430 res = tr.transform_bounds( 

431 left=ext_nor[0], 

432 bottom=ext_nor[1], 

433 right=ext_nor[2], 

434 top=ext_nor[3], 

435 errcheck=True, 

436 ) 

437 return _normalize_extent(res) 

438 

439 

440def _normalize_extent(ext): 

441 return ( 

442 min(ext[0], ext[2]), 

443 min(ext[1], ext[3]), 

444 max(ext[0], ext[2]), 

445 max(ext[1], ext[3]), 

446 ) 

447 

448 

449def _make_crs(srid, pp, axis, uom): 

450 crs = Object() 

451 

452 crs.srid = srid 

453 

454 with warnings.catch_warnings(): 

455 warnings.simplefilter('ignore') 

456 try: 

457 crs.proj4text = pp.to_proj4() 

458 except pyproj.exceptions.CRSError: 

459 gws.log.error(f'CRS: cannot convert {srid!r} to proj4') 

460 return None 

461 

462 crs.wkt = pp.to_wkt() 

463 

464 crs.axis = axis 

465 crs.uom = uom 

466 crs.coordinatePrecision = COORDINATE_PRECISION_M if uom == gws.Uom.m else COORDINATE_PRECISION_DEG 

467 

468 crs.isGeographic = pp.is_geographic 

469 crs.isProjected = pp.is_projected 

470 crs.isYX = crs.axis == gws.Axis.yx 

471 

472 crs.epsg = _unparse(crs.srid, gws.CrsFormat.epsg) 

473 crs.urn = _unparse(crs.srid, gws.CrsFormat.urn) 

474 crs.urnx = _unparse(crs.srid, gws.CrsFormat.urnx) 

475 crs.url = _unparse(crs.srid, gws.CrsFormat.url) 

476 crs.uri = _unparse(crs.srid, gws.CrsFormat.uri) 

477 

478 # see https://proj.org/schemas/v0.5/projjson.schema.json 

479 d = pp.to_json_dict() 

480 

481 crs.name = d.get('name') or str(crs.srid) 

482 

483 def _datum(x): 

484 if 'datum_ensemble' in x: 

485 return x['datum_ensemble']['name'] 

486 if 'datum' in x: 

487 return x['datum']['name'] 

488 return '' 

489 

490 def _bbox(d): 

491 b = d.get('bbox') 

492 if b: 

493 # pyproj 3.6 

494 return b 

495 if d.get('usages'): 

496 # pyproj 3.7 

497 for u in d['usages']: 

498 b = u.get('bbox') 

499 if b: 

500 return b 

501 

502 b = d.get('base_crs') 

503 if b: 

504 crs.base = b['id']['code'] 

505 crs.datum = _datum(b) 

506 else: 

507 crs.base = 0 

508 crs.datum = _datum(d) 

509 

510 b = _bbox(d) 

511 if not b: 

512 gws.log.error(f'CRS: no bbox for {crs.srid!r}') 

513 return 

514 

515 crs.wgsExtent = ( 

516 b['west_longitude'], 

517 b['south_latitude'], 

518 b['east_longitude'], 

519 b['north_latitude'], 

520 ) 

521 crs.extent = _transform_extent_check(crs.wgsExtent, WGS84.srid, srid) 

522 crs.bounds = gws.Bounds(extent=crs.extent, crs=crs) 

523 

524 return crs 

525 

526 

527_AXES_AND_UNITS = { 

528 'Easting/metre,Northing/metre': (gws.Axis.xy, gws.Uom.m), 

529 'Northing/metre,Easting/metre': (gws.Axis.yx, gws.Uom.m), 

530 'Geodetic latitude/degree,Geodetic longitude/degree': (gws.Axis.yx, gws.Uom.deg), 

531 'Geodetic longitude/degree,Geodetic latitude/degree': (gws.Axis.xy, gws.Uom.deg), 

532 'Easting/US survey foot,Northing/US survey foot': (gws.Axis.xy, gws.Uom.us_ft), 

533 'Easting/foot,Northing/foot': (gws.Axis.xy, gws.Uom.ft), 

534} 

535 

536 

537def _axis_and_unit(pp): 

538 ax = [] 

539 for a in pp.axis_info: 

540 ax.append(a.name + '/' + a.unit_name) 

541 return _AXES_AND_UNITS.get(','.join(ax)) 

542 

543 

544## 

545 

546""" 

547Projections can be referenced by: 

548 

549 - int/numeric SRID: 4326 

550 - EPSG Code: EPSG:4326 

551 - OGC HTTP URL: http://www.opengis.net/gml/srs/epsg.xml#4326 

552 - OGC Experimental URN: urn:x-ogc:def:crs:EPSG:4326 

553 - OGC URN: urn:ogc:def:crs:EPSG::4326 

554 - OGC HTTP URI: http://www.opengis.net/def/crs/EPSG/0/4326 

555 

556# https://docs.geoserver.org/stable/en/user/services/wfs/webadmin.html#gml 

557""" 

558 

559_WRITE_FORMATS = { 

560 gws.CrsFormat.srid: '{:d}', 

561 gws.CrsFormat.epsg: 'EPSG:{:d}', 

562 gws.CrsFormat.url: 'http://www.opengis.net/gml/srs/epsg.xml#{:d}', 

563 gws.CrsFormat.uri: 'http://www.opengis.net/def/crs/epsg/0/{:d}', 

564 gws.CrsFormat.urnx: 'urn:x-ogc:def:crs:EPSG:{:d}', 

565 gws.CrsFormat.urn: 'urn:ogc:def:crs:EPSG::{:d}', 

566} 

567 

568_PARSE_FORMATS = { 

569 gws.CrsFormat.srid: r'^(\d+)$', 

570 gws.CrsFormat.epsg: r'^epsg:(\d+)$', 

571 gws.CrsFormat.url: r'^http://www.opengis.net/gml/srs/epsg.xml#(\d+)$', 

572 gws.CrsFormat.uri: r'http://www.opengis.net/def/crs/epsg/0/(\d+)$', 

573 gws.CrsFormat.urnx: r'^urn:x-ogc:def:crs:epsg:(\d+)$', 

574 gws.CrsFormat.urn: r'^urn:ogc:def:crs:epsg:[0-9.]*:(\d+)$', 

575} 

576 

577# @TODO 

578 

579_aliases = { 

580 'crs:84': 4326, 

581 'crs84': 4326, 

582 'urn:ogc:def:crs:ogc:1.3:crs84': 'urn:ogc:def:crs:epsg::4326', 

583 'wgs84': 4326, 

584 'epsg:900913': 3857, 

585 'epsg:102100': 3857, 

586 'epsg:102113': 3857, 

587} 

588 

589# https://docs.geoserver.org/latest/en/user/services/wfs/axis_order.html 

590# EPSG:4326 longitude/latitude 

591# http://www.opengis.net/gml/srs/epsg.xml#xxxx longitude/latitude 

592# urn:x-ogc:def:crs:EPSG:xxxx latitude/longitude 

593# urn:ogc:def:crs:EPSG::4326 latitude/longitude 

594 

595_AXIS_FOR_FORMAT = { 

596 gws.CrsFormat.srid: gws.Axis.xy, 

597 gws.CrsFormat.epsg: gws.Axis.xy, 

598 gws.CrsFormat.url: gws.Axis.xy, 

599 gws.CrsFormat.uri: gws.Axis.xy, 

600 gws.CrsFormat.urnx: gws.Axis.yx, 

601 gws.CrsFormat.urn: gws.Axis.yx, 

602} 

603 

604 

605def _parse(crs_name): 

606 if isinstance(crs_name, int): 

607 return gws.CrsFormat.epsg, crs_name 

608 

609 if isinstance(crs_name, bytes): 

610 crs_name = crs_name.decode('ascii').lower() 

611 

612 if isinstance(crs_name, str): 

613 crs_name = crs_name.lower() 

614 

615 if crs_name in {'crs84', 'crs:84'}: 

616 return gws.CrsFormat.crs, 4326 

617 

618 if crs_name in _aliases: 

619 crs_name = _aliases[crs_name] 

620 if isinstance(crs_name, int): 

621 return gws.CrsFormat.epsg, int(crs_name) 

622 

623 for fmt, r in _PARSE_FORMATS.items(): 

624 m = re.match(r, crs_name) 

625 if m: 

626 return fmt, int(m.group(1)) 

627 

628 return None, 0 

629 

630 

631def _unparse(srid, fmt): 

632 return _WRITE_FORMATS[fmt].format(srid) 

633 

634 

635## 

636 

637 

638_obj_cache: dict = { 

639 WGS84.srid: WGS84, 

640 WEBMERCATOR.url: WEBMERCATOR, 

641} 

642 

643_pyproj_cache: dict = {} 

644 

645_transformer_cache: dict = {}