Coverage for gws-app/gws/plugin/mapshare/action.py: 0%
76 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
1"""The ``mapshare`` action."""
3from typing import Optional
5import re
7import gws
8import gws.base.action
9import gws.lib.shape
10import gws.lib.image
12MAX_TITLE_LENGTH = 200
13LINK_PARAM_NAME = 's'
16@gws.ext.config.action('mapshare')
17class Config(gws.base.action.Config):
18 """Shareable links and QR codes for a map position."""
20 pass
23@gws.ext.props.action('mapshare')
24class Props(gws.base.action.Props):
25 linkParamName: str = LINK_PARAM_NAME
28class CreateRequest(gws.Request):
29 """Request to create a share link."""
31 shape: gws.ShapeProps
32 """Shared shape."""
33 scale: Optional[int]
34 """Map scale."""
35 title: Optional[str]
36 """Title of the link."""
39class CreateLinkResponse(gws.Response):
40 """Share link and its QR code."""
42 url: str
43 """Link URL."""
44 qrCode: str
45 """QR code of the link, as a data URL."""
48class DecodeLinkRequest(gws.Request):
49 """Request to decode a share link."""
51 link: str
52 """Value of the ``s`` parameter of the link."""
55class DecodeLinkResponse(gws.Response):
56 """Decoded share link."""
58 shape: gws.ShapeProps
59 """Shared shape, in the CRS of the project map."""
60 scale: int
61 """Map scale, 0 if not given."""
62 title: str
63 """Title, empty if not given."""
66@gws.ext.object.action('mapshare')
67class Object(gws.base.action.Object):
68 """Map share action."""
70 def props(self, user):
71 return gws.u.merge(
72 super().props(user),
73 linkParamName=LINK_PARAM_NAME,
74 )
76 @gws.ext.command.api('mapshareCreateLink')
77 def create_link(self, req: gws.WebRequester, p: CreateRequest) -> CreateLinkResponse:
78 """Create a share link and its QR code for a map position."""
80 url = self._create_url(req, p)
81 return CreateLinkResponse(
82 url=url,
83 qrCode=gws.lib.image.qr_code(url).to_data_url(),
84 )
86 @gws.ext.command.api('mapshareDecodeLink')
87 def decode_link(self, req: gws.WebRequester, p: DecodeLinkRequest) -> DecodeLinkResponse:
88 """Decode a share link."""
90 project = req.user.require_project(p.projectUid)
91 shape, scale, title = self._decode_link(p.link, project.map.bounds.crs)
93 return DecodeLinkResponse(
94 shape=shape.to_props(),
95 scale=scale,
96 title=title,
97 )
99 def _create_url(self, req: gws.WebRequester, p: CreateRequest) -> str:
100 """Create the share URL for a position."""
101 project = req.user.require_project(p.projectUid)
102 crs = project.map.bounds.crs
104 try:
105 shape = gws.lib.shape.from_props(p.shape).transformed_to(crs)
106 except gws.lib.shape.Error:
107 raise gws.BadRequestError('mapshare: invalid shape')
109 title = (p.title or '').strip()[:MAX_TITLE_LENGTH]
111 e = self._encode_link(shape, p.scale or 0, title)
113 return req.canonical_url_for(
114 f'/_/webPage/name/project/projectUid/{project.uid}',
115 **{LINK_PARAM_NAME: e},
116 )
118 def _encode_link(self, shape: gws.Shape, scale: int, title: str) -> str:
119 """Encode a shape, scale and title as a link parameter."""
120 s = str(scale)
122 prec = 0 if shape.crs.uom == gws.Uom.m else 5
123 shape = shape.to_precision(prec)
125 if shape.type == gws.GeometryType.point:
126 x, y = shape.center()
127 s += f'x{x:.{prec}f}'
128 s += f'y{y:.{prec}f}'
129 else:
130 s += 'w' + shape.to_wkb_hex().lower()
132 title = re.sub(r'\s+', ' ', title).strip()
133 if title:
134 s += '_' + title
136 return s
138 LINK_RE = r'''(?x)
139 ^
140 (?P<scale>\d+)
141 (
142 (
143 x
144 (?P<x>-?\d+(\.\d+)?)
145 y
146 (?P<y>-?\d+(\.\d+)?)
147 )
148 |
149 (
150 w
151 (?P<wkb>[0-9a-f]+)
152 )
153 )
154 (
155 _
156 (?P<title>.+)
157 )?
158 $
159 '''
161 def _decode_link(self, link: str, crs: gws.Crs) -> tuple[gws.Shape, int, str]:
162 """Decode a link parameter into a shape, scale and title."""
163 m = re.match(self.LINK_RE, link)
164 if not m:
165 raise gws.BadRequestError('mapshare: invalid link')
167 try:
168 if m.group('wkb'):
169 shape = gws.lib.shape.from_wkb_hex(m.group('wkb'), crs,)
170 else:
171 shape = gws.lib.shape.from_xy(float(m.group('x')), float(m.group('y')), crs,)
172 except gws.lib.shape.Error:
173 raise gws.BadRequestError('mapshare: invalid link geometry')
175 title = (m.group('title') or '').strip()
177 return shape, int(m.group('scale')), title