Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,13 @@ the same scheme, host and port as the address of the capabilities. Otherwise the
the login, and msui warns once per session. It also warns when the capabilities were loaded with https and
the maps are to be requested with http.

WMS server list
~~~~~~~~~~~~~~~
The list of WMS URLs of a view starts with the default servers of that view (``default_WMS``,
``default_VSEC_WMS``, ``default_LSEC_WMS``), followed by every URL msui loaded capabilities from, most
recent first. The recently used URLs are shared by all views. To forget one, open the list, highlight the URL
and press Del or Backspace; default servers are removed only by changing the configuration.


MSUI Flight track import/export plugins
.......................................
Expand Down
10 changes: 6 additions & 4 deletions mslib/msui/qt5/ui_wms_multilayers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

# Form implementation generated from reading ui file 'mslib/msui/ui/ui_wms_multilayers.ui'
#
# Created by: PyQt5 UI code generator 5.12.3
# Created by: PyQt5 UI code generator 5.15.11
#
# WARNING! All changes made in this file will be lost!
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.


from PyQt5 import QtCore, QtGui, QtWidgets
Expand Down Expand Up @@ -97,7 +98,7 @@ def setupUi(self, MultilayersDialog):
MultilayersDialog.addAction(self.actionCloseWindow)

self.retranslateUi(MultilayersDialog)
self.actionCloseWindow.triggered.connect(MultilayersDialog.reject)
self.actionCloseWindow.triggered.connect(MultilayersDialog.reject) # type: ignore
QtCore.QMetaObject.connectSlotsByName(MultilayersDialog)

def retranslateUi(self, MultilayersDialog):
Expand All @@ -109,7 +110,8 @@ def retranslateUi(self, MultilayersDialog):
self.listLayers.headerItem().setText(2, _translate("MultilayersDialog", "Priority"))
self.listLayers.headerItem().setText(3, _translate("MultilayersDialog", "Colour"))
self.label_7.setText(_translate("MultilayersDialog", "WMS URL:"))
self.cbWMS_URL.setToolTip(_translate("MultilayersDialog", "Enter a valid WMS URL here."))
self.cbWMS_URL.setToolTip(_translate("MultilayersDialog", "Enter a valid WMS URL here.\n"
"In the opened list, Del or Backspace forgets the highlighted recently used URL."))
self.cbWMS_URL.setItemText(0, _translate("MultilayersDialog", "http://localhost:8002/fc_wms"))
self.cbWMS_URL.setItemText(1, _translate("MultilayersDialog", "http://osm.omniscale.net/proxy/service"))
self.btGetCapabilities.setToolTip(_translate("MultilayersDialog", "Request the capabilities from the WMS server."))
Expand Down
3 changes: 2 additions & 1 deletion mslib/msui/ui/ui_wms_multilayers.ui
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,8 @@
</sizepolicy>
</property>
<property name="toolTip">
<string>Enter a valid WMS URL here.</string>
<string>Enter a valid WMS URL here.
In the opened list, Del or Backspace forgets the highlighted recently used URL.</string>
</property>
<property name="styleSheet">
<string notr="true">QComboBox QAbstractItemView { min-width: 800px; }</string>
Expand Down
104 changes: 78 additions & 26 deletions mslib/msui/wms_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,30 @@


def add_wms_urls(combo_box, url_list):
combo_box_urls = [combo_box.itemText(_i) for _i in range(combo_box.count())]
for url in (_url for _url in url_list if _url not in combo_box_urls):
combo_box.addItem(url)
for url in url_list:
if combo_box.findText(url) == -1:
combo_box.addItem(url)


def load_recent_wms_urls():
"""Returns the WMS URLs msui connected to successfully, most recent first."""
wms_settings = load_settings_qsettings('wms', {'recent_wms_urls': [], 'recent_wms_url': None})
urls = list(wms_settings['recent_wms_urls'] or [])
# settings of msui versions that remembered only one URL
if wms_settings['recent_wms_url'] and wms_settings['recent_wms_url'] not in urls:
urls.append(wms_settings['recent_wms_url'])
return urls


def remember_wms_url(url):
"""Puts url at the front of the recently used WMS URLs and stores them."""
urls = [url] + [_url for _url in load_recent_wms_urls() if _url != url]
save_settings_qsettings('wms', {'recent_wms_urls': urls})


def forget_wms_url(url):
"""Removes url from the recently used WMS URLs."""
save_settings_qsettings('wms', {'recent_wms_urls': [_url for _url in load_recent_wms_urls() if _url != url]})


class MSUIWebMapServiceMixin:
Expand Down Expand Up @@ -547,12 +568,12 @@ def __init__(self, parent=None, default_WMS=None, wms_cache=None, view=None):

# Initial list of WMS servers.
self.multilayers.cbWMS_URL.setModel(QtGui.QStandardItemModel())
if default_WMS is not None:
add_wms_urls(self.multilayers.cbWMS_URL, default_WMS)
# set last connected url to editable
wms_settings = load_settings_qsettings('wms', {'recent_wms_url': None})
if wms_settings['recent_wms_url'] is not None:
add_wms_urls(self.multilayers.cbWMS_URL, [wms_settings['recent_wms_url']])
self.default_wms_urls = list(default_WMS or [])
add_wms_urls(self.multilayers.cbWMS_URL, self.default_wms_urls)
# WMS servers connected to recently
add_wms_urls(self.multilayers.cbWMS_URL, load_recent_wms_urls())
self.url_list_view = self.multilayers.cbWMS_URL.view()
self.url_list_view.installEventFilter(self)

self.layerChangeInProgress = False
self.save_level = None
Expand Down Expand Up @@ -899,23 +920,8 @@ def on_success(wms):
self.cpdlg.setValue(9)
if wms is not None:

# update the combo box, if entry requires change/insertion
found = False
current_url = self.multilayers.cbWMS_URL.currentText()
for count in range(self.multilayers.cbWMS_URL.count()):
if self.multilayers.cbWMS_URL.itemText(count) == current_url:
self.multilayers.cbWMS_URL.setItemText(count, base_url)
self.multilayers.cbWMS_URL.setCurrentIndex(count)
found = True
break
if self.multilayers.cbWMS_URL.itemText(count) == base_url:
self.multilayers.cbWMS_URL.setCurrentIndex(count)
found = True
if not found:
logging.debug("inserting URL: %s", base_url)
add_wms_urls(self.multilayers.cbWMS_URL, [base_url])
self.multilayers.cbWMS_URL.setEditText(base_url)
save_settings_qsettings('wms', {'recent_wms_url': base_url})
self.select_wms_url(base_url)
remember_wms_url(base_url)

self.activate_wms(wms, level=level)
self.service_manager.cache_service(base_url, wms)
Expand Down Expand Up @@ -1011,6 +1017,52 @@ def confirm_redirect(self, entered_url, url):
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No)
return ret == QtWidgets.QMessageBox.Yes

def select_wms_url(self, base_url):
"""
Selects base_url, the URL the capabilities were loaded from, in the URL combo box.

The URL as entered (e.g. without trailing slash, or before a redirect) is replaced by base_url,
or removed if base_url is listed already.
"""
combo = self.multilayers.cbWMS_URL
entered_index = combo.findText(combo.currentText())
if combo.findText(base_url) == -1:
if entered_index != -1:
combo.setItemText(entered_index, base_url)
else:
logging.debug("inserting URL: %s", base_url)
combo.addItem(base_url)
elif entered_index not in (-1, combo.findText(base_url)):
with QtCore.QSignalBlocker(combo):
combo.removeItem(entered_index)
combo.setCurrentIndex(combo.findText(base_url))

def eventFilter(self, obj, event):
"""Del or Backspace in the opened list of WMS URLs forgets the highlighted recently used URL."""
if obj is self.url_list_view and event.type() == QtCore.QEvent.KeyPress \
and event.key() in (QtCore.Qt.Key_Delete, QtCore.Qt.Key_Backspace):
index = self.url_list_view.currentIndex()
if index.isValid() and index.data() not in self.default_wms_urls:
self.forget_wms_server(index.data())
return True
return super().eventFilter(obj, event)

def forget_wms_server(self, url):
"""
Removes url from the recently used URLs and from the URL combo box. The entry stays in the
combo box while the server is loaded, as selecting its layers selects it there.
"""
forget_wms_url(url)
if url in self.multilayers.layers:
return
combo = self.multilayers.cbWMS_URL
text = combo.currentText()
# keep the entered text and do not load another server by changing it
with QtCore.QSignalBlocker(combo):
while (index := combo.findText(url)) != -1:
combo.removeItem(index)
combo.setEditText(text)

def warn_about_getmap_url(self, wms):
"""
Shows the warning of getmap_url_warning, once per session for each WMS and GetMap URL
Expand Down
91 changes: 90 additions & 1 deletion tests/_test_msui/test_wms_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
from xml.sax.saxutils import quoteattr
from PyQt5 import QtCore, QtTest, QtWidgets
from mslib.msui import flighttrack as ft
from mslib.msui.utils.config import config_loader
from mslib.msui.utils.config import config_loader, load_settings_qsettings, save_settings_qsettings
import mslib.msui.wms_control as wc
from tests.utils import shown_text
from mslib.msui.utils.service_manager import strip_request_params
Expand All @@ -55,6 +55,49 @@ class VSecViewMockup(mock.Mock):
get_plot_size_in_px = mock.Mock(return_value=(200, 100))


def test_recent_wms_urls():
# the single URL remembered by older msui versions
save_settings_qsettings("wms", {"recent_wms_url": "http://old/"})
assert wc.load_recent_wms_urls() == ["http://old/"]
for url in ["http://a/", "http://b/", "http://a/"]:
wc.remember_wms_url(url)
assert wc.load_recent_wms_urls() == ["http://a/", "http://b/", "http://old/"]
wc.forget_wms_url("http://b/")
assert wc.load_recent_wms_urls() == ["http://a/", "http://old/"]
save_settings_qsettings("wms", {"recent_wms_urls": ["http://single/"]})
assert wc.load_recent_wms_urls() == ["http://single/"]


def test_add_wms_urls_skips_duplicates(qtbot):
combo = QtWidgets.QComboBox()
qtbot.addWidget(combo)
combo.addItem("http://a/")
wc.add_wms_urls(combo, ["http://a/", "http://b/", "http://b/"])
assert [combo.itemText(i) for i in range(combo.count())] == ["http://a/", "http://b/"]


def press_in_url_list(combo, url, key=QtCore.Qt.Key_Delete):
"""Opens the list of the URL combo box, highlights url and presses key."""
combo.showPopup()
combo.view().setCurrentIndex(combo.model().index(combo.findText(url), 0))
QtTest.QTest.keyClick(combo.view(), key)
combo.hidePopup()


def test_delete_in_url_list_keeps_default_url(qtbot, tmp_path):
wc.remember_wms_url("http://recent/")
window = wc.HSecWMSControlWidget(default_WMS=["http://default/"], view=HSecViewMockup(), wms_cache=tmp_path)
qtbot.addWidget(window)
combo = window.multilayers.cbWMS_URL
combo.setEditText("http://typed/")
press_in_url_list(combo, "http://default/")
assert combo.findText("http://default/") != -1
press_in_url_list(combo, "http://recent/")
assert combo.findText("http://recent/") == -1
assert wc.load_recent_wms_urls() == []
assert combo.currentText() == "http://typed/"


class WMSControlWidgetSetup:
@pytest.fixture(autouse=True)
def _with_mswms_server(self, mswms_server):
Expand Down Expand Up @@ -448,6 +491,52 @@ def test_server_no_thread(self, mockthread, qtbot):
assert self.view.draw_legend.call_count == 1
assert self.view.draw_metadata.call_count == 1

def query_server_by_enter(self, qtbot, url):
"""Types url and presses Enter, which inserts it into the combo box, before loading the capabilities."""
self.window.multilayers.cbWMS_URL.setEditText("")
QtTest.QTest.keyClicks(self.window.multilayers.cbWMS_URL, url)
QtTest.QTest.keyClick(self.window.multilayers.cbWMS_URL, QtCore.Qt.Key_Return)
with qtbot.wait_signal(self.window.cpdlg.canceled, timeout=WMS_REQUEST_TIMEOUT_MS):
QtTest.QTest.mouseClick(self.window.multilayers.btGetCapabilities, QtCore.Qt.LeftButton)

def test_typed_url_is_remembered(self, qtbot):
self.query_server_by_enter(qtbot, self.url)
assert load_settings_qsettings("wms")["recent_wms_urls"][0] == f"{self.url}/"
other = wc.HSecWMSControlWidget(view=HSecViewMockup(), wms_cache=self.tempdir)
qtbot.addWidget(other)
assert other.multilayers.cbWMS_URL.findText(f"{self.url}/") != -1

def test_url_entered_again_is_listed_once(self, qtbot):
# entered without the trailing slash, loaded from the URL with it
self.query_server_by_enter(qtbot, self.url)
self.query_server_by_enter(qtbot, self.url)
combo = self.window.multilayers.cbWMS_URL
urls = [combo.itemText(i) for i in range(combo.count())]
assert urls.count(f"{self.url}/") == 1
assert self.url not in urls
assert combo.currentText() == f"{self.url}/"

@pytest.mark.parametrize("key", [QtCore.Qt.Key_Delete, QtCore.Qt.Key_Backspace], ids=["delete", "backspace"])
def test_delete_in_url_list_forgets_url(self, qtbot, key):
self.query_server_by_enter(qtbot, self.url)
url = f"{self.url}/"
# unloading the server keeps its URL
server = self.window.multilayers.listLayers.findItems(url, QtCore.Qt.MatchFixedString)[0]
self.window.multilayers.delete_server(server)
assert url in wc.load_recent_wms_urls()
combo = self.window.multilayers.cbWMS_URL
press_in_url_list(combo, url, key)
assert url not in wc.load_recent_wms_urls()
assert combo.findText(url) == -1

def test_delete_in_url_list_keeps_loaded_server_listed(self, qtbot):
self.query_server_by_enter(qtbot, self.url)
url = f"{self.url}/"
combo = self.window.multilayers.cbWMS_URL
press_in_url_list(combo, url)
assert url not in wc.load_recent_wms_urls()
assert combo.findText(url) != -1


class Test_VSecWMSControlWidget(WMSControlWidgetSetup):
@pytest.fixture(autouse=True)
Expand Down
5 changes: 3 additions & 2 deletions tests/msui_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@

# tests.constants sets MSUI_CONFIG_PATH, which mslib.msui.utils.config needs at import time
from tests.constants import create_msui_settings_file
from mslib.msui.utils.config import modify_config_file, read_config_file
from mslib.msui.utils.config import modify_config_file, read_config_file, save_settings_qsettings


class TestKeyring(keyring.backend.KeyringBackend):
Expand Down Expand Up @@ -70,9 +70,10 @@ def keyring_reset():

@pytest.fixture(autouse=True)
def reset_config():
"""Reset the msui configuration file used in the tests after every test."""
"""Reset the msui configuration file and the remembered WMS URLs used in the tests before every test."""
create_msui_settings_file("{}")
read_config_file()
save_settings_qsettings("wms", {})


@pytest.fixture
Expand Down
Loading