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
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-24 12:46 +0200
1import mapscript
2import re
4import gws
5import gws.lib.image
8def version() -> str:
9 """Returns the MapServer version string."""
11 return mapscript.msGetVersion()
14class Error(gws.Error):
15 pass
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}
27def new_map(config: str = '') -> 'Map':
28 """Creates a new Map instance from a Mapfile string."""
30 return Map(config)
33class Map:
34 """MapServer map object wrapper."""
36 mapObj: mapscript.mapObj
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()
46 self.mapObj.setConfigOption('MS_ERRORFILE', 'stderr')
47 # self.mapObj.debug = mapscript.MS_DEBUGLEVEL_DEVDEBUG
48 self.mapObj.debug = mapscript.MS_DEBUGLEVEL_ERRORSONLY
50 def copy(self) -> 'Map':
51 """Creates a copy of the current map object."""
53 c = Map()
54 c.mapObj = self.mapObj.clone()
55 return c
57 def add_layer_from_config(self, config: str) -> mapscript.layerObj:
58 """Adds a layer to the map using a configuration string."""
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
67 def add_layer(self, opts: gws.MapServerLayerOptions) -> mapscript.layerObj:
68 """Adds a layer to the map."""
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
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
82 if not opts.crs:
83 raise Error('missing layer CRS')
84 lo.setProjection(opts.crs.epsg)
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)
112 # @TODO: support style values
113 if opts.style:
114 cls = mapscript.classObj(lo)
116 if opts.style.with_geometry == 'all':
117 style_obj = self._create_style_obj(opts.style)
118 cls.insertStyle(style_obj)
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'
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)
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
141 def draw(self, bounds: gws.Bounds, size: gws.Size) -> gws.Image:
142 """Renders the map within the given bounds and size.
144 Args:
145 bounds: The spatial extent to render.
146 size: The output image size.
148 Returns:
149 The rendered map image.
150 """
152 # @TODO: options for image format, transparency, etc.
154 try:
155 gws.debug.time_start(f'mapserver.draw {bounds=} {size=}')
157 self.mapObj.setOutputFormat(mapscript.outputFormatObj('AGG/PNG'))
158 self.mapObj.outputformat.transparent = mapscript.MS_TRUE
160 self.mapObj.setExtent(*bounds.extent)
161 self.mapObj.setSize(int(size[0]), int(size[1]))
162 self.mapObj.setProjection(bounds.crs.epsg)
164 res = self.mapObj.draw()
165 img = gws.lib.image.from_bytes(res.getBytes())
167 gws.debug.time_end()
169 return img
171 except mapscript.MapServerError as exc:
172 raise Error(f'ms: draw error: {exc}') from exc
174 def to_string(self) -> str:
175 """Converts the map object to a configuration string."""
177 try:
178 return self.mapObj.convertToString()
179 except mapscript.MapServerError as exc:
180 raise Error(f'ms: convert error: {exc}') from exc
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
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
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
250 def style_symbol(self, style: gws.StyleValues) -> mapscript.styleObj:
251 mo = self.mapObj
252 so = mapscript.styleObj()
253 so.setSymbolByName(mo, style.marker)
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
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}')
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}
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}