44import re
55from collections .abc import Iterable
66from dataclasses import dataclass
7+ from io import BytesIO
78from pathlib import Path
89from typing import Any
910from urllib .request import urlopen
1011
1112import folium
13+ import numpy as np
14+ import pandas as pd
1215import shapely
13-
16+ from geopandas import GeoDataFrame
17+ from geopandas import GeoSeries
18+ from shapely import Geometry
19+ from shapely import get_exterior_ring
20+ from shapely import make_valid
21+ from shapely import polygons
22+ from shapely import set_precision
23+ from shapely import simplify
24+ from shapely .errors import GEOSException
25+
26+ from ..geopandas_tools .conversion import to_gdf
1427from ..geopandas_tools .conversion import to_shapely
28+ from ..geopandas_tools .sfilter import sfilter
29+ from ..raster .image_collection import Band
1530
1631JSON_PATH = Path (__file__ ).parent / "norge_i_bilder.json"
1732
18- JSON_YEARS = [str (year ) for year in range (1999 , 2025 )]
33+ JSON_YEARS = [str (year ) for year in range (2006 , datetime . datetime . now (). year + 1 )]
1934
2035DEFAULT_YEARS : tuple [str ] = tuple (
2136 str (year )
@@ -57,7 +72,7 @@ class NorgeIBilderWms(WmsLoader):
5772 show : bool | Iterable [int ] | int = False
5873 _use_json : bool = True
5974
60- def load_tiles (self ) -> None :
75+ def load_tiles (self , verbose : bool = False ) -> None :
6176 """Load all Norge i bilder tiles into self.tiles."""
6277 url = "https://wms.geonorge.no/skwms1/wms.nib-prosjekter?SERVICE=WMS&REQUEST=GetCapabilities"
6378
@@ -107,7 +122,10 @@ def load_tiles(self) -> None:
107122 this_tile ["name" ] = name
108123 this_tile ["bbox" ] = this_bbox
109124 year = name .split (" " )[- 1 ]
110- if year .isnumeric () and len (year ) == 4 :
125+ is_year_or_interval : bool = all (
126+ part .isnumeric () and len (part ) == 4 for part in year .split ("-" )
127+ )
128+ if is_year_or_interval :
111129 this_tile ["year" ] = year
112130 else :
113131 this_tile ["year" ] = "9999"
@@ -116,41 +134,123 @@ def load_tiles(self) -> None:
116134
117135 self .tiles = sorted (all_tiles , key = lambda x : x ["year" ])
118136
119- def get_tiles (self , bbox : Any , max_zoom : int = 40 ) -> list [folium .WmsTileLayer ]:
120- """Get all Norge i bilder tiles intersecting with a bbox."""
137+ masks = self ._get_norge_i_bilder_polygon_masks (verbose = verbose )
138+ for tile in self .tiles :
139+ mask = masks .get (tile ["name" ], None )
140+ tile ["geometry" ] = mask
141+
142+ def _get_norge_i_bilder_polygon_masks (self , verbose : bool ):
143+ from owslib .util import ServiceException
144+ from owslib .wms import WebMapService
145+ from PIL import Image
146+
147+ relevant_names : dict [str , str ] = {x ["name" ]: x ["bbox" ] for x in self .tiles }
148+ assert len (relevant_names ), relevant_names
149+
150+ url = "https://wms.geonorge.no/skwms1/wms.nib-mosaikk?SERVICE=WMS&REQUEST=GetCapabilities"
151+ wms = WebMapService (url , version = "1.3.0" )
152+ out = {}
153+ # ttiles = {wms[layer].title: [] for layer in list(wms.contents)}
154+ # for layer in list(wms.contents):
155+ # if wms[layer].title not in relevant_names:
156+ # continue
157+ # ttiles[wms[layer].title].append(layer)
158+ # import pandas as pd
159+
160+ # df = pd.Series(ttiles).to_frame("title")
161+ # df["n"] = df["title"].str.len()
162+ # df = df.sort_values("n")
163+ # for x in df["title"]:
164+ # if len(x) == 1:
165+ # continue
166+ # bounds = {tuple(wms[layer].boundingBoxWGS84) for layer in x}
167+ # if len(bounds) <= 1:
168+ # continue
169+ # print()
170+ # for layer in x:
171+ # print(layer)
172+ # print(wms[layer].title)
173+ # bbox = wms[layer].boundingBoxWGS84
174+ # print(bbox)
175+
176+ for layer in list (wms .contents ):
177+ title = wms [layer ].title
178+ if title not in relevant_names :
179+ continue
180+ bbox = wms [layer ].boundingBoxWGS84
181+ bbox = tuple (to_gdf (bbox , crs = 4326 ).to_crs (25832 ).total_bounds )
182+
183+ existing_bbox = relevant_names [title ]
184+ existing_bbox = to_gdf (existing_bbox , crs = 4326 ).to_crs (25832 ).union_all ()
185+ if not to_shapely (bbox ).intersects (existing_bbox ):
186+ continue
187+ diffx = bbox [2 ] - bbox [0 ]
188+ diffy = bbox [3 ] - bbox [1 ]
189+ width = int (diffx / 40 )
190+ height = int (diffy / 40 )
191+ if not bbox :
192+ continue
193+ try :
194+ img = wms .getmap (
195+ layers = [layer ],
196+ styles = ["" ], # Empty unless you know the style
197+ srs = "EPSG:25832" ,
198+ bbox = bbox ,
199+ size = (width , height ),
200+ format = "image/jpeg" ,
201+ transparent = True ,
202+ bgcolor = "#FFFFFF" ,
203+ )
204+ except (ServiceException , AttributeError ) as e :
205+ if verbose :
206+ print (type (e ), e )
207+ continue
208+
209+ arr = np .array (Image .open (BytesIO (img .read ())))
210+ if not np .sum (arr ):
211+ continue
212+
213+ band = Band (
214+ np .where (np .any (arr != 0 , axis = - 1 ), 1 , 0 ), bounds = bbox , crs = 25832
215+ )
216+ polygon = band .to_geopandas ()[lambda x : x ["value" ] == 1 ].geometry .values
217+ polygon = make_valid (polygons (get_exterior_ring (polygon )))
218+ polygon = make_valid (set_precision (polygon , 1 ))
219+ polygon = make_valid (simplify (polygon , 100 ))
220+ polygon = make_valid (set_precision (polygon , 1 ))
221+ polygon = GeoSeries (polygon , crs = 25832 ).to_crs (4326 )
222+ if verbose :
223+ print (f"Layer name: { layer } " )
224+ print (f"Title: { wms [layer ].title } " )
225+ print (f"Bounding box: { wms [layer ].boundingBoxWGS84 } " )
226+ print (f"polygon: { polygon } " )
227+ print ("-" * 40 )
228+
229+ for x in [0 , 0.1 , 0.001 , 1 ]:
230+ try :
231+ out [title ] = make_valid (polygon .buffer (x ).make_valid ().union_all ())
232+ except GEOSException :
233+ pass
234+ break
235+
236+ return out
237+
238+ def get_tiles (self , mask : Any , max_zoom : int = 40 ) -> list [folium .WmsTileLayer ]:
239+ """Get all Norge i bilder tiles intersecting with a mask (bbox or polygon)."""
121240 if self .tiles is None :
122241 self .load_tiles ()
123242
124- all_tiles = {}
125-
126- bbox = to_shapely (bbox )
243+ if not isinstance (mask , (GeoSeries | GeoDataFrame | Geometry )):
244+ mask = to_shapely (mask )
127245
128246 if isinstance (self .show , bool ):
129247 show = self .show
130248 else :
131249 show = False
132250
133- for tile in self .tiles :
134- if not tile ["bbox" ] or not tile ["bbox" ].intersects (bbox ):
135- continue
136-
137- name = tile ["name" ]
138-
139- if (
140- not name
141- or not any (year in name for year in self .years )
142- or (
143- self .contains
144- and not any (re .search (x , name .lower ()) for x in self .contains )
145- )
146- or (
147- self .not_contains
148- and any (re .search (x , name .lower ()) for x in self .not_contains )
149- )
150- ):
151- continue
152-
153- all_tiles [name ] = folium .WmsTileLayer (
251+ relevant_tiles = self ._filter_tiles (mask )
252+ tile_layers = {
253+ name : folium .WmsTileLayer (
154254 url = "https://wms.geonorge.no/skwms1/wms.nib-prosjekter" ,
155255 name = name ,
156256 layers = name ,
@@ -161,16 +261,37 @@ def get_tiles(self, bbox: Any, max_zoom: int = 40) -> list[folium.WmsTileLayer]:
161261 show = show ,
162262 max_zoom = max_zoom ,
163263 )
264+ for name in relevant_tiles ["name" ]
265+ }
266+
267+ if not len (tile_layers ):
268+ return tile_layers
164269
165270 if isinstance (self .show , int ):
166- tile = all_tiles [list (all_tiles )[self .show ]]
271+ tile = tile_layers [list (tile_layers )[self .show ]]
167272 tile .show = True
168273 elif isinstance (self .show , Iterable ):
169274 for i in self .show :
170- tile = all_tiles [list (all_tiles )[i ]]
275+ tile = tile_layers [list (tile_layers )[i ]]
171276 tile .show = True
172277
173- return all_tiles
278+ return tile_layers
279+
280+ def _filter_tiles (self , mask ):
281+ """Filter relevant dates with pandas and geopandas because fast."""
282+ df = pd .DataFrame (self .tiles )
283+ filt = (df ["name" ].notna ()) & (df ["year" ].str .contains ("|" .join (self .years )))
284+ if self .contains :
285+ for x in self .contains :
286+ filt &= df ["name" ].str .contains (x )
287+ if self .not_contains :
288+ for x in self .not_contains :
289+ filt &= ~ df ["name" ].str .contains (x )
290+ df = df [filt ]
291+ geoms = np .where (df ["geometry" ].notna (), df ["geometry" ], df ["bbox" ])
292+ geoms = GeoSeries (geoms )
293+ assert geoms .index .is_unique
294+ return df .iloc [sfilter (geoms , mask ).index ]
174295
175296 def __post_init__ (self ) -> None :
176297 """Fix typings."""
@@ -195,7 +316,11 @@ def __post_init__(self) -> None:
195316 return
196317 self .tiles = [
197318 {
198- key : value if key != "bbox" else shapely .wkt .loads (value )
319+ key : (
320+ value
321+ if key not in ["bbox" , "geometry" ]
322+ else shapely .wkt .loads (value )
323+ )
199324 for key , value in tile .items ()
200325 }
201326 for tile in self .tiles
0 commit comments