Skip to content

Commit

Permalink
Use extent instead of center position and zoom (#119)
Browse files Browse the repository at this point in the history
* Remove the lat/long from options and use the extent instead

* update python test

* Use extent in open layer (after merging #112)

* Update examples with extent option

* Fix python test

* Set the extent as optionnal, use the lat/long by default

* lint
  • Loading branch information
brichet authored Sep 6, 2024
1 parent 0cb79ce commit 21ff340
Show file tree
Hide file tree
Showing 8 changed files with 87 additions and 146 deletions.
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:
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

0 comments on commit 21ff340

Please sign in to comment.