Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use extent instead of center position and zoom #119

Merged
merged 8 commits into from
Sep 6, 2024
Merged
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
3 changes: 2 additions & 1 deletion examples/3d_terrain.jGIS
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
"latitude": 47.25967099028631,
"longitude": 11.418052209432549,
"pitch": 59.00000000000003,
"zoom": 11.829283007646955
"projection": "EPSG:3857",
"zoom": 11.829291680317894
},
"sources": {
"ceef4036-b757-44bf-8a21-42c6c99dab72": {
Expand Down
53 changes: 34 additions & 19 deletions packages/base/src/mainview/mainView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
VectorTile as VectorTileLayer,
WebGLTile as WebGlTileLayer
} from 'ol/layer';
import TileLayer from 'ol/layer/Tile';
import BaseLayer from 'ol/layer/Base';
import { fromLonLat, toLonLat } from 'ol/proj';
import Feature from 'ol/render/Feature';
Expand All @@ -46,13 +47,13 @@ import {
} from 'ol/source';
import Static from 'ol/source/ImageStatic';
import { Circle, Fill, Stroke, Style } from 'ol/style';
//@ts-expect-error no types for ol-pmtiles
import { PMTilesRasterSource, PMTilesVectorSource } from 'ol-pmtiles';
import * as React from 'react';
import { isLightTheme } from '../tools';
import { MainViewModel } from './mainviewmodel';
import { Spinner } from './spinner';
//@ts-expect-error no types for ol-pmtiles
import { PMTilesRasterSource, PMTilesVectorSource } from 'ol-pmtiles';
import TileLayer from 'ol/layer/Tile';
import { isLightTheme } from '../tools';

interface IProps {
viewModel: MainViewModel;
}
Expand Down Expand Up @@ -184,23 +185,32 @@ export class MainView extends React.Component<IProps, IStates> {
return;
}

const currentOptions = this._model.getOptions();

const view = this._Map.getView();
const center = view.getCenter();
const zoom = view.getZoom();
if (!center || !zoom) {
return;
}
const center = view.getCenter() || [0, 0];
const zoom = view.getZoom() || 0;

const projection = view.getProjection();
const latLng = toLonLat(center, projection);
const bearing = view.getRotation();

this._model.setOptions({
...this._model.getOptions(),
const updatedOptions: Partial<IJGISOptions> = {
latitude: latLng[1],
longitude: latLng[0],
bearing,
projection: projection.getCode(),
zoom
};

// Update the extent only if has been initially provided.
if (currentOptions.extent) {
updatedOptions.extent = view.calculateExtent();
}

this._model.setOptions({
...currentOptions,
...updatedOptions
});
});

Expand Down Expand Up @@ -831,15 +841,20 @@ export class MainView extends React.Component<IProps, IStates> {
}

private updateOptions(options: IJGISOptions) {
const centerCoord = fromLonLat(
[options.longitude, options.latitude],
this._Map.getView().getProjection()
);

this._Map.getView().setZoom(options.zoom || 0);
this._Map.getView().setCenter(centerCoord || [0, 0]);
const view = this._Map.getView();

this._Map.getView().setRotation(options.bearing || 0);
// use the extent if provided.
if (options.extent) {
view.fit(options.extent);
} else {
const centerCoord = fromLonLat(
[options.longitude || 0, options.latitude || 0],
this._Map.getView().getProjection()
);
this._Map.getView().setZoom(options.zoom || 0);
this._Map.getView().setCenter(centerCoord);
}
view.setRotation(options.bearing || 0);
}

private _onViewChanged(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ const FilterComponent = (props: IFilterComponentProps) => {
}
const layer = model.getLayer(currentLayer ?? selectedLayer);
const source = model.getSource(layer?.parameters?.source);
const { latitude, longitude, zoom } = model.getOptions();
const { latitude, longitude, extent, zoom } = model.getOptions();

if (!source || !layer) {
return;
Expand Down Expand Up @@ -187,6 +187,7 @@ const FilterComponent = (props: IFilterComponentProps) => {
const tile = await getLayerTileInfo(source?.parameters?.url, {
latitude,
longitude,
extent,
zoom
});
const layerValue = tile.layers[layer.parameters?.sourceLayer];
Expand Down
20 changes: 12 additions & 8 deletions packages/base/src/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ function getTileCoordinates(latDeg: number, lonDeg: number, zoom: number) {

export async function getLayerTileInfo(
tileUrl: string,
mapOptions: Pick<IJGISOptions, 'latitude' | 'longitude' | 'zoom'>,
mapOptions: Pick<IJGISOptions, 'latitude' | 'longitude' | 'extent' | 'zoom'>,
urlParameters?: IDict<string>
): Promise<VectorTile> {
// If it's tilejson, fetch the json to access the pbf url
Expand All @@ -247,15 +247,19 @@ export async function getLayerTileInfo(
tileUrl = json.tiles[0];
}

const { xTile, yTile } = getTileCoordinates(
mapOptions.latitude,
mapOptions.longitude,
mapOptions.zoom
);
const latitude = mapOptions.extent
? (mapOptions.extent[1] + mapOptions.extent[3]) / 2
: mapOptions.latitude || 0;
const longitude = mapOptions.extent
? (mapOptions.extent[0] + mapOptions.extent[2]) / 2
: mapOptions.longitude || 0;
const zoom = mapOptions.zoom || 0;

const { xTile, yTile } = getTileCoordinates(latitude, longitude, zoom);

// Replace url params with currently viewed tile
tileUrl = tileUrl
.replace('{z}', String(Math.floor(mapOptions.zoom)))
.replace('{z}', String(Math.floor(zoom)))
.replace('{x}', String(xTile))
.replace('{y}', String(yTile));

Expand All @@ -282,7 +286,7 @@ export async function getSourceLayerNames(
) {
const tile = await getLayerTileInfo(
tileUrl,
{ latitude: 0, longitude: 0, zoom: 0 },
{ latitude: 0, longitude: 0, zoom: 0, extent: [] },
urlParameters
);

Expand Down
16 changes: 8 additions & 8 deletions packages/schema/src/schema/jgis.json
Original file line number Diff line number Diff line change
Expand Up @@ -168,14 +168,7 @@
"title": "IJGISOptions",
"type": "object",
"default": {},
"required": [
"latitude",
"longitude",
"zoom",
"bearing",
"pitch",
"projection"
],
"required": [],
"additionalProperties": false,
"properties": {
"latitude": {
Expand All @@ -198,6 +191,13 @@
"type": "number",
"default": 0
},
"extent": {
"type": "array",
"default": null,
"items": {
"type": "number"
}
},
"projection": {
"type": "string",
"default": "EPSG:3857"
Expand Down
25 changes: 14 additions & 11 deletions python/jupytergis_lab/jupytergis_lab/notebook/gis_document.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import json
import logging
from pathlib import Path
from typing import Any, Dict, List, Optional, Union
from typing import Any, Dict, List, Literal, Optional, Union

from pycrdt import Array, Doc, Map
from pydantic import BaseModel
Expand Down Expand Up @@ -43,12 +43,13 @@ class GISDocument(CommWidget):
def __init__(
self,
path: Optional[str] = None,
latitude: Optional[number] = None,
longitude: Optional[number] = None,
zoom: Optional[number] = None,
bearing: Optional[number] = None,
pitch: Optional[number] = None,
projection: Optional[string] = None
latitude: Optional[float] = None,
longitude: Optional[float] = None,
zoom: Optional[float] = None,
extent: Optional[List[float]] = None,
bearing: Optional[float] = None,
pitch: Optional[float] = None,
projection: Optional[str] = None
):
comm_metadata = GISDocument._path_to_comm(path)

Expand All @@ -69,6 +70,8 @@ def __init__(
self._options["latitude"] = latitude
if longitude is not None:
self._options["longitude"] = longitude
if extent is not None:
self._options["extent"] = extent
if zoom is not None:
self._options["zoom"] = zoom
if bearing is not None:
Expand Down Expand Up @@ -139,15 +142,15 @@ def add_vectortile_layer(
name: str = "Vector Tile Layer",
source_layer: str | None = None,
attribution: str = "",
min_zoom: number = 0,
max_zoom: number = 24,
type: "circle" | "fill" | "line" = "line",
min_zoom: int = 0,
max_zoom: int = 24,
type: Literal["circle", "fill", "line"] = "line",
color: str = "#FF0000",
opacity: float = 1,
logical_op:str | None = None,
feature:str | None = None,
operator:str | None = None,
value:Union[str, number, float] | None = None
value:Union[str, float, float] | None = None
):

"""
Expand Down
104 changes: 9 additions & 95 deletions python/jupytergis_qgis/jupytergis_qgis/qgis_loader.py
Original file line number Diff line number Diff line change
@@ -1,93 +1,21 @@
from __future__ import annotations

from pathlib import Path
from typing import Any
from urllib.parse import unquote
from uuid import uuid4

from qgis.PyQt.QtCore import QSize
from qgis.core import (
QgsApplication,
QgsLayerTreeGroup,
QgsLayerTreeLayer,
QgsRasterLayer,
QgsVectorTileLayer,
QgsProject,
QgsMapSettings,
QgsCoordinateReferenceSystem,
QgsCoordinateTransform,
QgsReferencedRectangle,
)

from jupytergis_lab.notebook.utils import get_source_layer_names


# Part of this code is copied from https://github.com/felt/qgis-plugin (GPL-2.0 license)
class MapUtils:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cleaning

ZOOM_LEVEL_SCALE_BREAKS = [
591657527.591555,
295828763.795777,
147914381.897889,
73957190.948944,
36978595.474472,
18489297.737236,
9244648.868618,
4622324.434309,
2311162.217155,
1155581.108577,
577790.554289,
288895.277144,
144447.638572,
72223.819286,
36111.909643,
18055.954822,
9027.977411,
4513.988705,
2256.994353,
1128.497176,
564.248588,
282.124294,
141.062147,
70.5310735,
]

@staticmethod
def map_scale_to_tile_zoom(scale: float) -> int:
"""
Returns the tile zoom level roughly
corresponding to a QGIS map scale
"""
for level, min_scale in enumerate(MapUtils.ZOOM_LEVEL_SCALE_BREAKS):
if min_scale < scale:
# we play it safe and zoom out a step -- this is because
# we don't know the screen size or DPI on which the map
# will actually be viewed, so we err on the conservative side
return level - 1

return len(MapUtils.ZOOM_LEVEL_SCALE_BREAKS) - 1

@staticmethod
def calculate_tile_zoom_for_extent(
extent: QgsReferencedRectangle,
target_map_size: QSize,
) -> int:
"""
Calculates the required leaflet tile zoom level in order
to completely fit a specified extent.

:param extent: required minimum map extent
:param target_map_size: size of leaflet map, in pixels
"""

map_settings = QgsMapSettings()
map_settings.setDestinationCrs(extent.crs())
map_settings.setExtent(extent)
map_settings.setOutputDpi(96)
map_settings.setOutputSize(target_map_size)

scale = map_settings.scale()
return MapUtils.map_scale_to_tile_zoom(scale)


def qgis_layer_to_jgis(
qgis_layer: QgsLayerTreeLayer,
layers: dict[str, dict[str, Any]],
Expand Down Expand Up @@ -214,34 +142,20 @@ def import_project_from_qgis(path: str | Path):

jgis_layer_tree = qgis_layer_tree_to_jgis(layer_tree_root)

# Infer zoom level and center
# TODO Extract projection type when we support multiple types
# extract the viewport in lat/long coordinates
view_settings = project.viewSettings()
current_map_extent = view_settings.defaultViewExtent()
current_map_crs = view_settings.defaultViewExtent().crs()
transform_context = project.transformContext()

transform_4326 = QgsCoordinateTransform(
current_map_crs, QgsCoordinateReferenceSystem("EPSG:4326"), transform_context
)
try:
map_extent_4326 = transform_4326.transformBoundingBox(current_map_extent)
except QgsCsException:
map_extent_4326 = current_map_extent

map_center = map_extent_4326.center()

initial_zoom_level = MapUtils.calculate_tile_zoom_for_extent(
QgsReferencedRectangle(current_map_extent, current_map_crs), QSize(1024, 800)
)
map_extent = view_settings.defaultViewExtent()

return {
"options": {
"bearing": 0.0,
"pitch": 0,
"latitude": map_center[1],
"longitude": map_center[0],
"zoom": initial_zoom_level,
"extent": [
map_extent.xMinimum(),
map_extent.yMinimum(),
map_extent.xMaximum(),
map_extent.yMaximum()
]
},
**jgis_layer_tree,
}
Loading
Loading