1 |
bh |
2086 |
# Copyright (C) 2003, 2004 by Intevation GmbH |
2 |
jan |
1944 |
# Authors: |
3 |
|
|
# Jan-Oliver Wagner <[email protected]> |
4 |
|
|
# |
5 |
|
|
# This program is free software under the GPL (>=v2) |
6 |
|
|
# Read the file COPYING coming with Thuban for details. |
7 |
|
|
|
8 |
|
|
""" |
9 |
|
|
Provide layers via OGC WMS. |
10 |
|
|
|
11 |
|
|
This extension is in a very experimental stage! |
12 |
|
|
It just demonstrates how to add a special |
13 |
|
|
layer into Thuban via an extension. |
14 |
|
|
Some things are not wired, so be prepared for Exceptions |
15 |
|
|
everywhere. |
16 |
|
|
|
17 |
|
|
You will need PyOGCLib 0.1.0, see |
18 |
|
|
http://pyogclib.sourceforge.net/ |
19 |
|
|
Set the PYTHONPATH to the PyOGCLib directory before |
20 |
|
|
starting Thuban. |
21 |
|
|
""" |
22 |
|
|
|
23 |
|
|
__version__ = "$Revision$" |
24 |
|
|
|
25 |
|
|
import os, sys |
26 |
|
|
import xml.dom.minidom |
27 |
|
|
import tempfile |
28 |
|
|
|
29 |
|
|
from wxPython.wx import * |
30 |
|
|
|
31 |
|
|
from ogclib.WMSClient import WMSClient |
32 |
|
|
|
33 |
|
|
from Thuban.Model.layer import BaseLayer |
34 |
|
|
from Thuban.Model.proj import Projection |
35 |
|
|
from Thuban.Model.extension import Extension |
36 |
bh |
2086 |
from Thuban.Model.resource import get_system_proj_file, EPSG_PROJ_FILE, \ |
37 |
|
|
EPSG_DEPRECATED_PROJ_FILE |
38 |
jan |
1944 |
from Thuban.UI.command import registry, Command |
39 |
|
|
import Thuban.UI.mainwindow |
40 |
joey |
2158 |
from Thuban.UI.common import ThubanBeginBusyCursor, ThubanEndBusyCursor |
41 |
jan |
1944 |
from Thuban import _ |
42 |
|
|
import Thuban.UI.baserenderer |
43 |
|
|
|
44 |
joey |
2154 |
from capabilities import WMSCapabilities |
45 |
jan |
1944 |
|
46 |
|
|
def epsg_code_to_projection(epsg): |
47 |
|
|
"""Find the projection for the given epsg code. |
48 |
|
|
|
49 |
bh |
2086 |
epsg -- EPSG code as string |
50 |
jan |
1944 |
""" |
51 |
bh |
2086 |
proj_file, warnings = get_system_proj_file(EPSG_PROJ_FILE) |
52 |
|
|
|
53 |
jan |
1944 |
for proj in proj_file.GetProjections(): |
54 |
|
|
if proj.EPSGCode() == epsg: |
55 |
|
|
return proj |
56 |
bh |
2086 |
proj_file, warnings = get_system_proj_file(EPSG_DEPRECATED_PROJ_FILE) |
57 |
jan |
1944 |
for proj in proj_file.GetProjections(): |
58 |
|
|
if proj.EPSGCode() == epsg: |
59 |
|
|
return proj |
60 |
|
|
return None |
61 |
|
|
|
62 |
|
|
class WMSExtension(Extension): |
63 |
|
|
def TreeInfo(self): |
64 |
|
|
return (_("Extension: %s") % self.title, |
65 |
|
|
[ object.TreeInfo() for object in self.objects ]) |
66 |
|
|
|
67 |
joey |
2154 |
class WMSLayer(BaseLayer): |
68 |
jan |
1944 |
|
69 |
|
|
def __init__(self, title, url): |
70 |
|
|
"""Initializes the WMSLayer. |
71 |
|
|
|
72 |
|
|
title -- Title of this layer. |
73 |
|
|
url -- URL of the WMS-Server wich must contain '?' |
74 |
|
|
|
75 |
|
|
If an error occured, self.error_msg is a string describing |
76 |
|
|
the problem(s). Else, self.error_msg is None. |
77 |
|
|
""" |
78 |
|
|
BaseLayer.__init__(self, title, visible = True, projection = None) |
79 |
|
|
self.url = url |
80 |
|
|
self.bbox = None |
81 |
|
|
self.latlonbbox = None |
82 |
|
|
self.error_msg = None |
83 |
|
|
self.layer_name = None |
84 |
joey |
2154 |
self.capabilities = None |
85 |
jan |
1944 |
|
86 |
joey |
2154 |
# Change the cursor to demonstrate that we're busy but working |
87 |
joey |
2158 |
ThubanBeginBusyCursor() |
88 |
joey |
2154 |
self.capabilities = WMSCapabilities(url) |
89 |
joey |
2158 |
ThubanEndBusyCursor() |
90 |
jan |
1944 |
|
91 |
joey |
2154 |
# name of the top layer of the remote map |
92 |
|
|
foo = self.capabilities.getLayers() |
93 |
|
|
if len(foo) == 0: |
94 |
|
|
self.error_msg = _('No layers found in remote resource:\n'\ |
95 |
|
|
'%s') % url |
96 |
|
|
return |
97 |
|
|
top_layer = foo[0] |
98 |
|
|
self.layer_name = top_layer |
99 |
jan |
1944 |
|
100 |
joey |
2154 |
# first projection of the top layer |
101 |
|
|
foo = self.capabilities.getLayerSRS(top_layer) |
102 |
|
|
if len(foo) == 0: |
103 |
|
|
self.error_msg = _('No LatLonBoundingBox found for top layer %s')\ |
104 |
|
|
% top_layer |
105 |
|
|
return |
106 |
|
|
top_srs = foo[0] |
107 |
jan |
1944 |
|
108 |
joey |
2154 |
# BoundingBox of the top layer |
109 |
|
|
bbox = self.capabilities.getLayerBBox(top_layer, top_srs) |
110 |
|
|
if len(bbox) == 0: |
111 |
|
|
self.error_msg = _('No BoundingBox found for layer %s and EPSG:')\ |
112 |
|
|
% (top_layer, top_srs) |
113 |
|
|
return |
114 |
|
|
self.bbox = (float(bbox['minx']), |
115 |
|
|
float(bbox['miny']), |
116 |
|
|
float(bbox['maxx']), |
117 |
|
|
float(bbox['maxy'])) |
118 |
|
|
|
119 |
|
|
# LatLonBox of the top layer |
120 |
|
|
bbox = self.capabilities.getLayerLatLonBBox(top_layer) |
121 |
|
|
self.latlonbbox = (float(bbox['minx']), |
122 |
|
|
float(bbox['miny']), |
123 |
|
|
float(bbox['maxx']), |
124 |
|
|
float(bbox['maxy'])) |
125 |
|
|
|
126 |
jan |
1944 |
# get projection |
127 |
joey |
2154 |
p = epsg_code_to_projection(top_srs) |
128 |
jan |
1944 |
self.SetProjection(p) |
129 |
|
|
|
130 |
|
|
if p is None: |
131 |
|
|
self.error_msg = _('EPSG projection code %s not found!\n'\ |
132 |
|
|
'Setting projection to "None".\n'\ |
133 |
|
|
'Please set an appropriate projection yourself.'\ |
134 |
|
|
% epsg_id) |
135 |
|
|
|
136 |
joey |
2154 |
# pre-determine the used format |
137 |
|
|
self.wmsformat, self.format = \ |
138 |
|
|
self.calcFormat(self.capabilities.getFormats()) |
139 |
|
|
if self.wmsformat is None: |
140 |
|
|
self.error_msg = \ |
141 |
|
|
_('No supported image format found in remote resource') |
142 |
|
|
return |
143 |
jan |
1944 |
|
144 |
joey |
2154 |
# get and set the title |
145 |
|
|
self.SetTitle(self.capabilities.getTitle().encode('latin1', 'replace')) |
146 |
jan |
1944 |
|
147 |
joey |
2154 |
|
148 |
jan |
1944 |
def LatLongBoundingBox(self): |
149 |
|
|
"""Return the layer's bounding box in lat-lon. |
150 |
|
|
""" |
151 |
|
|
return self.latlonbbox |
152 |
|
|
|
153 |
|
|
def BoundingBox(self): |
154 |
|
|
"""Return the layer's bounding box in the intrinsic coordinate system. |
155 |
|
|
""" |
156 |
|
|
return self.bbox |
157 |
|
|
|
158 |
joey |
2154 |
|
159 |
|
|
def getFormat(self, format): |
160 |
|
|
""" |
161 |
|
|
Return the image format for the render engine |
162 |
|
|
|
163 |
|
|
format -- format as returned by the WMS server |
164 |
|
|
|
165 |
|
|
If no mapping was found, None is returned |
166 |
|
|
|
167 |
|
|
An exception rule is implemented in order to not accept |
168 |
|
|
image/wbmp or WBMP which refers to WAP bitmap format and is |
169 |
|
|
not supported by the included render engine. |
170 |
|
|
""" |
171 |
|
|
fmap = {'png' : "PNG", |
172 |
|
|
'jpeg': "JPEG", |
173 |
|
|
'jpg' : "JPEG", |
174 |
|
|
'tif' : "TIFF", |
175 |
|
|
'gif' : "GIF", |
176 |
|
|
'wbmp': None, |
177 |
|
|
'bmp' : "BMP"} |
178 |
|
|
|
179 |
|
|
for f in fmap.keys(): |
180 |
|
|
if format.lower().find(f) > -1: |
181 |
|
|
return fmap[f] |
182 |
|
|
return None |
183 |
|
|
|
184 |
|
|
|
185 |
|
|
def calcFormat(self, formats): |
186 |
|
|
""" |
187 |
|
|
Calculate the preferred image format |
188 |
|
|
|
189 |
|
|
formats -- list of formates as returned by the WMS server |
190 |
|
|
|
191 |
|
|
The following priority is used: |
192 |
|
|
- PNG |
193 |
|
|
- JPEG |
194 |
|
|
- TIFF |
195 |
|
|
- GIF |
196 |
|
|
- BMP |
197 |
|
|
|
198 |
|
|
If no matching format was found, None, None will be returned. |
199 |
|
|
|
200 |
|
|
An exception rule is implemented in order to not accept |
201 |
|
|
image/wbmp or WBMP which refers to WAP bitmap format and is |
202 |
|
|
not supported by the included render engine. |
203 |
|
|
""" |
204 |
|
|
prio = ['png', 'jpeg', 'jpg', 'tif', 'gif', 'bmp'] |
205 |
|
|
for p in prio: |
206 |
|
|
for f in formats: |
207 |
|
|
if f.lower().find(p) > -1: |
208 |
|
|
if f.lower().find('wbmp') == -1: |
209 |
|
|
return f, self.getFormat(f) |
210 |
|
|
return None, None |
211 |
|
|
|
212 |
|
|
|
213 |
jan |
1944 |
def GetMapImg(self, width, height, bbox): |
214 |
|
|
bbox_dict = { 'minx': bbox[0], 'miny': bbox[1], |
215 |
|
|
'maxx': bbox[2], 'maxy': bbox[3] } |
216 |
joey |
2154 |
|
217 |
|
|
# Change the cursor to demonstrate that we're busy but working |
218 |
joey |
2158 |
ThubanBeginBusyCursor() |
219 |
joey |
2154 |
|
220 |
|
|
wmsclient = WMSClient() |
221 |
|
|
|
222 |
jan |
1944 |
epsg_id = int(self.GetProjection().EPSGCode()) |
223 |
joey |
2154 |
|
224 |
|
|
wms_response = wmsclient.getMap(self.url, self.wmsformat, width, height, |
225 |
jan |
1944 |
epsg_id, bbox_dict, |
226 |
joey |
2154 |
[self.layer_name], version = self.capabilities.getVersion()) |
227 |
joey |
2158 |
ThubanEndBusyCursor() |
228 |
joey |
2154 |
return wms_response, self.format |
229 |
jan |
1944 |
|
230 |
|
|
|
231 |
|
|
def render_wms_layer(renderer, layer): |
232 |
|
|
offx, offy = renderer.offset |
233 |
|
|
width, height = renderer.dc.GetSizeTuple() |
234 |
|
|
|
235 |
|
|
scale = renderer.scale |
236 |
|
|
xmin = (0 - offx) / scale |
237 |
|
|
ymin = (offy - height) / scale |
238 |
|
|
xmax = (width - offx) / scale |
239 |
|
|
ymax = (offy - 0) / scale |
240 |
|
|
|
241 |
joey |
2154 |
img, format = layer.GetMapImg(width, height, (xmin, ymin, xmax, ymax)) |
242 |
|
|
renderer.draw_raster_data(img, format) |
243 |
jan |
1944 |
|
244 |
|
|
return () |
245 |
|
|
|
246 |
|
|
Thuban.UI.baserenderer.add_renderer_extension(WMSLayer, render_wms_layer) |
247 |
|
|
|
248 |
|
|
|
249 |
|
|
class SelectWMSServer(wxDialog): |
250 |
|
|
|
251 |
|
|
ID_COMBOVALUE = 4003 |
252 |
|
|
|
253 |
|
|
def __init__(self, parent): |
254 |
|
|
wxDialog.__init__(self, parent, -1, _("Select WMS Server"), |
255 |
|
|
style = wxDEFAULT_DIALOG_STYLE |
256 |
|
|
| wxSYSTEM_MENU |
257 |
|
|
| wxRESIZE_BORDER) |
258 |
|
|
|
259 |
|
|
self.combo_value = wxComboBox(self, self.ID_COMBOVALUE, size=(500,-1)) |
260 |
|
|
self.combo_value.Append("") |
261 |
|
|
self.combo_value.Append('http://frida.intevation.org/cgi-bin/frida_wms?') |
262 |
|
|
#self.combo_value.Append('http://wms.jpl.nasa.gov/wms.cgi?') |
263 |
|
|
#self.combo_value.Append('http://eukrante.hq:9089/cgi-bin/wms_shg?') |
264 |
|
|
#self.combo_value.Append('http://131.220.106.112:8080/deegree0.7/wms?') |
265 |
|
|
#self.combo_value.Append('http://demo.cubewerx.com/demo/cubeserv/cubeserv.cgi?CONFIG=gita&') |
266 |
|
|
self.combo_value.SetSelection(0) |
267 |
|
|
|
268 |
|
|
button_ok = wxButton(self, wxID_OK, _("OK")) |
269 |
|
|
button_ok.SetDefault() |
270 |
|
|
button_close = wxButton(self, wxID_CANCEL, _("Close")) |
271 |
|
|
|
272 |
|
|
vbox = wxBoxSizer(wxVERTICAL) |
273 |
|
|
vbox.Add(self.combo_value, 1, wxEXPAND|wxALL|wxCB_SORT, 10) |
274 |
|
|
hbox = wxBoxSizer(wxHORIZONTAL) |
275 |
|
|
hbox.Add(button_ok, 0, wxALL, 10) |
276 |
|
|
hbox.Add(button_close, 0, wxALL, 10) |
277 |
|
|
vbox.Add(hbox, 0, 10) |
278 |
|
|
|
279 |
|
|
self.SetAutoLayout(True) |
280 |
|
|
self.SetSizer(vbox) |
281 |
|
|
vbox.Fit(self) |
282 |
|
|
vbox.SetSizeHints(self) |
283 |
|
|
self.Layout() |
284 |
|
|
|
285 |
|
|
EVT_BUTTON(self, wxID_OK, self.OnOK) |
286 |
|
|
EVT_BUTTON(self, wxID_CANCEL, self.OnCancel) |
287 |
|
|
|
288 |
|
|
def OnOK(self, event): |
289 |
|
|
self.url = self.combo_value.GetValue() |
290 |
|
|
self.EndModal(wxID_OK) |
291 |
|
|
|
292 |
|
|
def OnCancel(self, event): |
293 |
|
|
self.EndModal(wxID_CANCEL) |
294 |
|
|
|
295 |
|
|
def wms_dialog(context): |
296 |
|
|
"""Request URL from user and add WMS Layer. |
297 |
|
|
|
298 |
|
|
context -- The Thuban context. |
299 |
|
|
""" |
300 |
|
|
dialog = SelectWMSServer(context.mainwindow) |
301 |
|
|
|
302 |
|
|
if dialog.ShowModal() == wxID_OK: |
303 |
|
|
url = dialog.url |
304 |
|
|
else: |
305 |
|
|
url = None |
306 |
|
|
dialog.Destroy() |
307 |
|
|
|
308 |
|
|
if url is None: |
309 |
|
|
return |
310 |
|
|
|
311 |
|
|
wms_layer = WMSLayer('A WMS Layer', url) |
312 |
|
|
if wms_layer.error_msg is not None: |
313 |
|
|
context.mainwindow.RunMessageBox(_('WMS'), wms_layer.error_msg) |
314 |
|
|
|
315 |
|
|
map = context.mainwindow.canvas.Map() |
316 |
|
|
if map.projection is None: |
317 |
|
|
map.SetProjection(wms_layer.projection) |
318 |
|
|
has_layers = map.HasLayers() |
319 |
|
|
map.AddLayer(wms_layer) |
320 |
|
|
if not has_layers: |
321 |
|
|
# if we're adding a layer to an empty map, fit the |
322 |
|
|
# new map to the window |
323 |
|
|
context.mainwindow.canvas.FitMapToWindow() |
324 |
|
|
|
325 |
|
|
wxInitAllImageHandlers() |
326 |
|
|
wms_extension = WMSExtension('WMS') |
327 |
|
|
|
328 |
|
|
# register the new command |
329 |
|
|
registry.Add(Command('wms', _('Add WMS layer ...'), wms_dialog, |
330 |
|
|
helptext = _('Add a WMS Layer'))) |
331 |
|
|
|
332 |
|
|
# find the experimental menu (create it anew if not found) |
333 |
|
|
main_menu = Thuban.UI.mainwindow.main_menu |
334 |
|
|
experimental_menu = main_menu.find_menu('experimental') |
335 |
|
|
if experimental_menu is None: |
336 |
|
|
experimental_menu = main_menu.InsertMenu('experimental', _('Experimenta&l')) |
337 |
|
|
|
338 |
|
|
# finally add the new entry to the experimental menu |
339 |
|
|
experimental_menu.InsertItem('wms') |