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

1"""The ``mapshare`` action.""" 

2 

3from typing import Optional 

4 

5import re 

6 

7import gws 

8import gws.base.action 

9import gws.lib.shape 

10import gws.lib.image 

11 

12MAX_TITLE_LENGTH = 200 

13LINK_PARAM_NAME = 's' 

14 

15 

16@gws.ext.config.action('mapshare') 

17class Config(gws.base.action.Config): 

18 """Shareable links and QR codes for a map position.""" 

19 

20 pass 

21 

22 

23@gws.ext.props.action('mapshare') 

24class Props(gws.base.action.Props): 

25 linkParamName: str = LINK_PARAM_NAME 

26 

27 

28class CreateRequest(gws.Request): 

29 """Request to create a share link.""" 

30 

31 shape: gws.ShapeProps 

32 """Shared shape.""" 

33 scale: Optional[int] 

34 """Map scale.""" 

35 title: Optional[str] 

36 """Title of the link.""" 

37 

38 

39class CreateLinkResponse(gws.Response): 

40 """Share link and its QR code.""" 

41 

42 url: str 

43 """Link URL.""" 

44 qrCode: str 

45 """QR code of the link, as a data URL.""" 

46 

47 

48class DecodeLinkRequest(gws.Request): 

49 """Request to decode a share link.""" 

50 

51 link: str 

52 """Value of the ``s`` parameter of the link.""" 

53 

54 

55class DecodeLinkResponse(gws.Response): 

56 """Decoded share link.""" 

57 

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.""" 

64 

65 

66@gws.ext.object.action('mapshare') 

67class Object(gws.base.action.Object): 

68 """Map share action.""" 

69 

70 def props(self, user): 

71 return gws.u.merge( 

72 super().props(user), 

73 linkParamName=LINK_PARAM_NAME, 

74 ) 

75 

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.""" 

79 

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 ) 

85 

86 @gws.ext.command.api('mapshareDecodeLink') 

87 def decode_link(self, req: gws.WebRequester, p: DecodeLinkRequest) -> DecodeLinkResponse: 

88 """Decode a share link.""" 

89 

90 project = req.user.require_project(p.projectUid) 

91 shape, scale, title = self._decode_link(p.link, project.map.bounds.crs) 

92 

93 return DecodeLinkResponse( 

94 shape=shape.to_props(), 

95 scale=scale, 

96 title=title, 

97 ) 

98 

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 

103 

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') 

108 

109 title = (p.title or '').strip()[:MAX_TITLE_LENGTH] 

110 

111 e = self._encode_link(shape, p.scale or 0, title) 

112 

113 return req.canonical_url_for( 

114 f'/_/webPage/name/project/projectUid/{project.uid}', 

115 **{LINK_PARAM_NAME: e}, 

116 ) 

117 

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) 

121 

122 prec = 0 if shape.crs.uom == gws.Uom.m else 5 

123 shape = shape.to_precision(prec) 

124 

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() 

131 

132 title = re.sub(r'\s+', ' ', title).strip() 

133 if title: 

134 s += '_' + title 

135 

136 return s 

137 

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 ''' 

160 

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') 

166 

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') 

174 

175 title = (m.group('title') or '').strip() 

176 

177 return shape, int(m.group('scale')), title