Coverage for gws-app/gws/lib/mapserver/core.py: 77%

222 statements  

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

1import mapscript 

2import re 

3 

4import gws 

5import gws.lib.image 

6 

7 

8def version() -> str: 

9 """Returns the MapServer version string.""" 

10 

11 return mapscript.msGetVersion() 

12 

13 

14class Error(gws.Error): 

15 pass 

16 

17 

18_LAYER_TYPE_TO_MS = { 

19 gws.MapServerLayerType.point: mapscript.MS_LAYER_POINT, 

20 gws.MapServerLayerType.line: mapscript.MS_LAYER_LINE, 

21 gws.MapServerLayerType.polygon: mapscript.MS_LAYER_POLYGON, 

22 gws.MapServerLayerType.raster: mapscript.MS_LAYER_RASTER, 

23} 

24 

25 

26 

27def new_map(config: str = '') -> 'Map': 

28 """Creates a new Map instance from a Mapfile string.""" 

29 

30 return Map(config) 

31 

32 

33class Map: 

34 """MapServer map object wrapper.""" 

35 

36 mapObj: mapscript.mapObj 

37 

38 def __init__(self, config: str = ''): 

39 if config: 

40 tmp = gws.c.EPHEMERAL_DIR + '/mapse_' + gws.u.random_string(16) + '.map' 

41 gws.u.write_file(tmp, config) 

42 self.mapObj = mapscript.mapObj(tmp) 

43 else: 

44 self.mapObj = mapscript.mapObj() 

45 

46 self.mapObj.setConfigOption('MS_ERRORFILE', 'stderr') 

47 # self.mapObj.debug = mapscript.MS_DEBUGLEVEL_DEVDEBUG 

48 self.mapObj.debug = mapscript.MS_DEBUGLEVEL_ERRORSONLY 

49 

50 def copy(self) -> 'Map': 

51 """Creates a copy of the current map object.""" 

52 

53 c = Map() 

54 c.mapObj = self.mapObj.clone() 

55 return c 

56 

57 def add_layer_from_config(self, config: str) -> mapscript.layerObj: 

58 """Adds a layer to the map using a configuration string.""" 

59 

60 try: 

61 lo = mapscript.layerObj(self.mapObj) 

62 lo.updateFromString(config) 

63 return lo 

64 except mapscript.MapServerError as exc: 

65 raise Error(f'ms: add error:: {exc}') from exc 

66 

67 def add_layer(self, opts: gws.MapServerLayerOptions) -> mapscript.layerObj: 

68 """Adds a layer to the map.""" 

69 

70 try: 

71 lo = self._make_layer(opts) 

72 return lo 

73 except mapscript.MapServerError as exc: 

74 raise Error(f'ms: add error:: {exc}') from exc 

75 

76 def _make_layer(self, opts: gws.MapServerLayerOptions) -> mapscript.layerObj: 

77 lo = mapscript.layerObj(self.mapObj) 

78 lc = self.mapObj.numlayers 

79 lo.name = f'_gws_{lc}' 

80 lo.status = mapscript.MS_ON 

81 

82 if not opts.crs: 

83 raise Error('missing layer CRS') 

84 lo.setProjection(opts.crs.epsg) 

85 

86 if opts.type: 

87 lo.type = _LAYER_TYPE_TO_MS[opts.type] 

88 if opts.path: 

89 lo.data = opts.path 

90 if opts.tileIndex: 

91 lo.tileindex = opts.tileIndex 

92 if opts.processing: 

93 for p in opts.processing: 

94 lo.addProcessing(p) 

95 if opts.transparentColor: 

96 r, g, b, a = _css_color_to_rgb(opts.transparentColor) 

97 co = mapscript.colorObj() 

98 co.setRGB(r, g, b, a) 

99 lo.offsite = co 

100 if opts.connectionType: 

101 if opts.connectionType == 'postgres': 

102 lo.setConnectionType(mapscript.MS_POSTGIS, '') 

103 else: 

104 raise Error(f'unsupported connectionType {opts.connectionType!r}') 

105 if opts.connectionString: 

106 lo.connection = opts.connectionString 

107 if opts.dataString: 

108 lo.data = opts.dataString 

109 if opts.sldPath: 

110 lo.applySLD(gws.u.read_file(opts.sldPath), opts.sldName) 

111 

112 # @TODO: support style values 

113 if opts.style: 

114 cls = mapscript.classObj(lo) 

115 

116 if opts.style.with_geometry == 'all': 

117 style_obj = self._create_style_obj(opts.style) 

118 cls.insertStyle(style_obj) 

119 

120 if opts.style.with_label == 'all': 

121 label_obj = self._create_label_obj(opts.style) 

122 cls.addLabel(label_obj) 

123 lo.labelitem = 'label' 

124 

125 if opts.style.marker or opts.style.icon: 

126 if opts.style.marker: 

127 self.mapObj.setSymbolSet('/gws-app/gws/lib/mapserver/symbolset.sym') 

128 so = self.style_symbol(opts.style) 

129 cls.insertStyle(so) 

130 

131 if opts.style.icon: 

132 symbol = mapscript.symbolObj('icon', opts.style.icon) 

133 symbol.type = mapscript.MS_SYMBOL_PIXMAP 

134 lo.map.symbolset.appendSymbol(symbol) 

135 so = mapscript.styleObj() 

136 so.setSymbolByName(lo.map, 'icon') 

137 so.size = 100 

138 cls.insertStyle(so) 

139 return lo 

140 

141 def draw(self, bounds: gws.Bounds, size: gws.Size) -> gws.Image: 

142 """Renders the map within the given bounds and size. 

143 

144 Args: 

145 bounds: The spatial extent to render. 

146 size: The output image size. 

147 

148 Returns: 

149 The rendered map image. 

150 """ 

151 

152 # @TODO: options for image format, transparency, etc. 

153 

154 try: 

155 gws.debug.time_start(f'mapserver.draw {bounds=} {size=}') 

156 

157 self.mapObj.setOutputFormat(mapscript.outputFormatObj('AGG/PNG')) 

158 self.mapObj.outputformat.transparent = mapscript.MS_TRUE 

159 

160 self.mapObj.setExtent(*bounds.extent) 

161 self.mapObj.setSize(int(size[0]), int(size[1])) 

162 self.mapObj.setProjection(bounds.crs.epsg) 

163 

164 res = self.mapObj.draw() 

165 img = gws.lib.image.from_bytes(res.getBytes()) 

166 

167 gws.debug.time_end() 

168 

169 return img 

170 

171 except mapscript.MapServerError as exc: 

172 raise Error(f'ms: draw error: {exc}') from exc 

173 

174 def to_string(self) -> str: 

175 """Converts the map object to a configuration string.""" 

176 

177 try: 

178 return self.mapObj.convertToString() 

179 except mapscript.MapServerError as exc: 

180 raise Error(f'ms: convert error: {exc}') from exc 

181 

182 def _create_style_obj(self, style: gws.StyleValues) -> mapscript.styleObj: 

183 so = mapscript.styleObj() 

184 if style.fill: 

185 so.color.setRGB(*_css_color_to_rgb(style.fill)) 

186 if style.stroke: 

187 so.outlinecolor.setRGB(*_css_color_to_rgb(style.stroke)) 

188 so.outlinewidth = max(0.1 * style.stroke_width, 1) 

189 if style.stroke_dasharray: 

190 so.pattern_set(style.stroke_dasharray) 

191 if style.stroke_dashoffset: 

192 so.gap = style.stroke_dashoffset 

193 if style.stroke_linecap: 

194 so.linecap = _const_mapping.get(style.stroke_linecap.lower()) 

195 if style.stroke_linejoin: 

196 so.linejoin = _const_mapping.get(style.stroke_linejoin.lower()) 

197 if style.stroke_miterlimit: 

198 so.linejoinmaxsize = style.stroke_miterlimit 

199 if style.stroke_width: 

200 so.width = style.stroke_width 

201 if style.offset_x: 

202 so.offsetx = style.offset_x 

203 if style.offset_y: 

204 so.offsety = style.offset_y 

205 return so 

206 

207 def _create_label_obj(self, style: gws.StyleValues) -> mapscript.labelObj: 

208 lo = mapscript.labelObj() 

209 so = mapscript.styleObj() 

210 lo.force = mapscript.MS_TRUE 

211 

212 if style.label_align: 

213 lo.align = _const_mapping.get(style.label_align) 

214 if style.label_background: 

215 so.setGeomTransform('labelpoly') 

216 so.color.setRGB(*_css_color_to_rgb(style.label_background)) 

217 if style.label_fill: 

218 lo.color.setRGB(*_css_color_to_rgb(style.label_fill)) 

219 if style.label_font_family: 

220 lo.font = style.label_font_family # + '-' + style.label_font_style + '-' + style.label_font_weight 

221 if style.label_font_size: 

222 lo.size = style.label_font_size 

223 if style.label_max_scale: 

224 lo.maxscaledenom = style.label_max_scale 

225 if style.label_min_scale: 

226 lo.minscaledenom = style.label_min_scale 

227 if style.label_offset_x: 

228 lo.offsetx = style.label_offset_x 

229 if style.label_offset_y: 

230 lo.offsety = style.label_offset_y 

231 if style.label_padding: 

232 lo.buffer = max(style.label_padding) 

233 if style.label_placement: 

234 lo.position = _const_mapping.get(style.label_placement) 

235 if style.label_stroke: 

236 lo.outlinecolor.setRGB(*_css_color_to_rgb(style.label_stroke)) 

237 if style.label_stroke_dasharray: 

238 so.pattern_set(style.label_stroke_dasharray) 

239 if style.label_stroke_linecap: 

240 so.linecap = _const_mapping.get(style.label_stroke_linecap.lower()) 

241 if style.label_stroke_linejoin: 

242 so.linejoin = _const_mapping.get(style.label_stroke_linejoin.lower()) 

243 if style.label_stroke_miterlimit: 

244 so.linejoinmaxsize = style.label_stroke_miterlimit 

245 if style.label_stroke_width: 

246 lo.outlinewidth = style.label_stroke_width 

247 lo.insertStyle(so) 

248 return lo 

249 

250 def style_symbol(self, style: gws.StyleValues) -> mapscript.styleObj: 

251 mo = self.mapObj 

252 so = mapscript.styleObj() 

253 so.setSymbolByName(mo, style.marker) 

254 

255 if style.marker_fill: 

256 so.color.setRGB(*_css_color_to_rgb(style.marker_fill)) 

257 if style.marker_size: 

258 so.size = style.marker_size 

259 if style.marker_stroke: 

260 so.outlinecolor.setRGB(*_css_color_to_rgb(style.marker_stroke)) 

261 if style.marker_stroke_dasharray: 

262 so.pattern_set(style.marker_stroke_dasharray) 

263 if style.marker_stroke_dashoffset: 

264 so.gap = style.marker_stroke_dashoffset 

265 if style.marker_stroke_linecap: 

266 so.linecap = _const_mapping.get(style.marker_stroke_linecap.lower()) 

267 if style.marker_stroke_linejoin: 

268 so.linejoin = _const_mapping.get(style.marker_stroke_linejoin.lower()) 

269 if style.marker_stroke_miterlimit: 

270 so.linejoinmaxsize = style.marker_stroke_miterlimit 

271 if style.marker_stroke_width: 

272 so.outlinewidth = style.marker_stroke_width 

273 return so 

274 

275 

276def _css_color_to_rgb(s: str) -> tuple[int, int, int, int]: 

277 s = re.sub(r'\s+', '', s).strip().lower() 

278 if s in _CSS_COLOR_NAMES: 

279 r, g, b = _CSS_COLOR_NAMES[s] 

280 return r, g, b, 255 

281 m = re.match(r'^#([0-9a-f]{6})$', s) 

282 if m: 

283 h = m.group(1) 

284 return int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16), 255 

285 m = re.match(r'^#([0-9a-f]{8})$', s) 

286 if m: 

287 h = m.group(1) 

288 return int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16), int(h[6:8], 16) 

289 m = re.match(r'^rgb\((\d+),(\d+),(\d+)\)$', s) 

290 if m: 

291 return int(m.group(1)), int(m.group(2)), int(m.group(3)), 255 

292 m = re.match(r'^rgba\((\d+),(\d+),(\d+),(\d+)\)$', s) 

293 if m: 

294 return int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4)) 

295 raise ValueError(f'invalid color string: {s!r}') 

296 

297 

298_CSS_COLOR_NAMES = { 

299 'black': (0, 0, 0), 

300 'white': (255, 255, 255), 

301 'red': (255, 0, 0), 

302 'lime': (0, 255, 0), 

303 'blue': (0, 0, 255), 

304 'yellow': (255, 255, 0), 

305 'cyan': (0, 255, 255), 

306 'aqua': (0, 255, 255), 

307 'magenta': (255, 0, 255), 

308 'fuchsia': (255, 0, 255), 

309 'gray': (128, 128, 128), 

310 'grey': (128, 128, 128), 

311 'maroon': (128, 0, 0), 

312 'olive': (128, 128, 0), 

313 'green': (0, 128, 0), 

314 'purple': (128, 0, 128), 

315 'teal': (0, 128, 128), 

316 'navy': (0, 0, 128), 

317} 

318 

319_const_mapping = { 

320 'butt': mapscript.MS_CJC_BUTT, 

321 'round': mapscript.MS_CJC_ROUND, 

322 'square': mapscript.MS_CJC_SQUARE, 

323 'bevel': mapscript.MS_CJC_BEVEL, 

324 'miter': mapscript.MS_CJC_MITER, 

325 'left': mapscript.MS_ALIGN_LEFT, 

326 'center': mapscript.MS_ALIGN_CENTER, 

327 'right': mapscript.MS_ALIGN_RIGHT, 

328 'start': mapscript.MS_CL, 

329 'middle': mapscript.MS_CC, 

330 'end': mapscript.MS_CR, 

331}