Skip to content

Commit

Permalink
qgis proxy
Browse files Browse the repository at this point in the history
  • Loading branch information
sunu committed Jun 4, 2024
0 parents commit af28295
Show file tree
Hide file tree
Showing 21 changed files with 488 additions and 0 deletions.
136 changes: 136 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# Extra ignore patterns specific to this project
# Installed JS libraries
node_modules/
# Built JS files
jupyter_remote_desktop_proxy/static/dist

# Standard python gitignore patterns
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
.python-version

# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock

# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/
59 changes: 59 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
FROM quay.io/jupyter/base-notebook:latest

USER root

RUN apt-get -y -qq update \
&& apt-get -y -qq install \
dbus-x11 \
xfce4 \
xfce4-panel \
xfce4-session \
xfce4-settings \
xorg \
xubuntu-icon-theme \
fonts-dejavu \
# Disable the automatic screenlock since the account password is unknown
&& apt-get -y -qq remove xfce4-screensaver \
# chown $HOME to workaround that the xorg installation creates a
# /home/jovyan/.cache directory owned by root
# Create /opt/install to ensure it's writable by pip
&& mkdir -p /opt/install \
&& chown -R $NB_UID:$NB_GID $HOME /opt/install \
&& rm -rf /var/lib/apt/lists/*

# Install a VNC server, either TigerVNC (default) or TurboVNC
ARG vncserver=tigervnc
RUN if [ "${vncserver}" = "tigervnc" ]; then \
echo "Installing TigerVNC"; \
apt-get -y -qq update; \
apt-get -y -qq install \
tigervnc-standalone-server \
tigervnc-xorg-extension \
; \
rm -rf /var/lib/apt/lists/*; \
fi
ENV PATH=/opt/TurboVNC/bin:$PATH
RUN if [ "${vncserver}" = "turbovnc" ]; then \
echo "Installing TurboVNC"; \
# Install instructions from https://turbovnc.org/Downloads/YUM
wget -q -O- https://packagecloud.io/dcommander/turbovnc/gpgkey | \
gpg --dearmor >/etc/apt/trusted.gpg.d/TurboVNC.gpg; \
wget -O /etc/apt/sources.list.d/TurboVNC.list https://raw.githubusercontent.com/TurboVNC/repo/main/TurboVNC.list; \
apt-get -y -qq update; \
apt-get -y -qq install \
turbovnc \
; \
rm -rf /var/lib/apt/lists/*; \
fi

USER $NB_USER

# Install the environment first, and then install the package separately for faster rebuilds
COPY --chown=$NB_UID:$NB_GID environment.yml /tmp
RUN . /opt/conda/bin/activate && \
mamba env update --quiet --file /tmp/environment.yml

COPY --chown=$NB_UID:$NB_GID . /opt/install
RUN . /opt/conda/bin/activate && \
pip install -e /opt/install && \
jupyter server extension enable jupyter_remote_qgis_proxy
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
graft jupyter_remote_qgis_proxy/qgis/templates
Empty file added README.md
Empty file.
11 changes: 11 additions & 0 deletions environment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Unfortunately the version of websockify on PyPI doesn't include the [compiled
# wrapper library](https://github.com/novnc/websockify#wrap-a-program) which is
# used by this extension, so either you'd have to manually compile it after pip
# installing websockify, or use the conda package.
#
channels:
- conda-forge
dependencies:
- jupyterhub-singleuser
- pip
- websockify
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"NotebookApp": {
"nbserver_extensions": {
"jupyter_remote_qgis_proxy": true
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"ServerApp": {
"jpserver_extensions": {
"jupyter_remote_qgis_proxy": true
}
}
}
Binary file added jupyter_remote_qgis_proxy/.DS_Store
Binary file not shown.
17 changes: 17 additions & 0 deletions jupyter_remote_qgis_proxy/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import os

from .server_extension import load_jupyter_server_extension

HERE = os.path.dirname(os.path.abspath(__file__))


def _jupyter_server_extension_points():
"""
Set up the server extension for QGIS proxy
"""
return [{"module": "jupyter_remote_qgis_proxy"}]


# For backward compatibility
_load_jupyter_server_extension = load_jupyter_server_extension
_jupyter_server_extension_paths = _jupyter_server_extension_points
23 changes: 23 additions & 0 deletions jupyter_remote_qgis_proxy/handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import os
import logging

from jupyter_remote_desktop_proxy.handlers import DesktopHandler
from tornado import web

from .qgis.utils import open_qgis

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


class QgisHandler(DesktopHandler):
@web.authenticated
async def get(self):
logging.info("Starting QGIS")
action = self.get_argument("action", None)
url = self.get_argument("url", None)
project_name = self.get_argument("project_name", "new project")
layer_name = self.get_argument("layer_name", "Vector Layer")
if action and url:
open_qgis(action, url=url, project_name=project_name, layer_name=layer_name)
await super().get()
Binary file added jupyter_remote_qgis_proxy/qgis/.DS_Store
Binary file not shown.
Empty file.
Empty file.
6 changes: 6 additions & 0 deletions jupyter_remote_qgis_proxy/qgis/scripts/maximize.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Maximize the QGIS window."""
import qgis.utils

# Get the QGIS interface
iface = qgis.utils.iface
iface.mainWindow().showMaximized()
18 changes: 18 additions & 0 deletions jupyter_remote_qgis_proxy/qgis/scripts/zoom.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""Zoom to the full extent of the first layer in the project"""

from qgis.core import QgsProject
import qgis.utils

# Get the QGIS interface
iface = qgis.utils.iface

# Get the active map canvas
canvas = iface.mapCanvas()

layers = QgsProject.instance().mapLayers().values()

for layer in layers:
# Zoom to the full extent of the layer
canvas.setExtent(layer.extent())
canvas.refresh()
break
14 changes: 14 additions & 0 deletions jupyter_remote_qgis_proxy/qgis/templates/add_vector_layer.qgs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
<qgis projectname="" version="3.36.2-Maidenhead">
<layer-tree-group>
<layer-tree-layer id="{layer_id}" source="{layer_src}">
</layer-tree-layer>
</layer-tree-group>
<projectlayers>
<maplayer type="vector">
<id>{layer_id}</id>
<datasource>{layer_src}</datasource>
<layername>{layer_name}</layername>
</maplayer>
</projectlayers>
</qgis>
31 changes: 31 additions & 0 deletions jupyter_remote_qgis_proxy/qgis/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import pathlib
import subprocess
import uuid

def open_qgis(action="add_vector_layer", **kwargs):
current_file_path = pathlib.Path(__file__).parent.resolve()
# script to zoom to the first layer
zoom_script_path = f"{current_file_path}/scripts/zoom.py"
# script to maximize the window
maximize_script_path = f"{current_file_path}/scripts/maximize.py"
if not action or not kwargs.get("url"):
subprocess.Popen(["qgis", "--nologo", "--code", maximize_script_path])
return

available_templates = ("add_vector_layer",)
if action not in available_templates:
raise ValueError(f"Action {action} not available. Choose from {available_templates}")

with open(f"{current_file_path}/templates/{action}.qgs") as f:
action_template = f.read()
project_name = kwargs.get("project_name", "new project")
file_path = f"/tmp/{project_name}.qgs"
with open(file_path, "w") as f:
layer_args = {
"layer_name": kwargs.get("layer_name", "Vector Layer"),
"layer_src": f"/vsicurl/{kwargs['url']}",
"layer_id": str(uuid.uuid4()),
}
project_file_content = action_template.format(**layer_args)
f.write(project_file_content)
subprocess.Popen(["qgis", "--nologo", "--project", file_path, "--code", zoom_script_path, "--code", maximize_script_path])
23 changes: 23 additions & 0 deletions jupyter_remote_qgis_proxy/server_extension.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from pathlib import Path

from jupyter_server.utils import url_path_join
from jupyter_server_proxy.handlers import AddSlashHandler

from .handlers import QgisHandler

HERE = Path(__file__).parent


def load_jupyter_server_extension(server_app):
"""
Called during notebook start
"""
base_url = server_app.web_app.settings["base_url"]

server_app.web_app.add_handlers(
".*",
[
(url_path_join(base_url, "/qgis"), AddSlashHandler),
(url_path_join(base_url, "/qgis/"), QgisHandler),
],
)
7 changes: 7 additions & 0 deletions jupyter_remote_qgis_proxy/setup_websockify.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from jupyter_remote_desktop_proxy.setup_websockify import setup_websockify as _setup_websockify


def setup_websockify():
config = _setup_websockify()
config["launcher_entry"] = {"title": "QGIS", "path_info": "qgis"}
return config
Loading

0 comments on commit af28295

Please sign in to comment.