"""
Author: HECE - University of Liege, Pierre Archambeau
Date: 2024
Copyright (c) 2024 University of Liege. All rights reserved.
This script and its content are protected by copyright law. Unauthorized
copying or distribution of this file, via any medium, is strictly prohibited.
"""
from PIL import Image
from .cached_images import CachedImages
from .async_image_loader import get_global_loader, ImageLoadPriority
try:
from OpenGL.GL import *
from OpenGL.GLUT import *
except:
[docs]
msg=_('Error importing OpenGL library')
msg+=_(' Python version : ' + sys.version)
msg+=_(' Please check your version of opengl32.dll -- conflict may exist between different files present on your desktop')
raise Exception(msg)
from os.path import exists
from pathlib import Path
from io import BytesIO
import logging
import numpy as np
from .PyTranslate import _
from .PyWMS import getIGNFrance, getWalonmap, getVlaanderen, getLifeWatch, getNGI, getCartoweb, getOrthoPostFlood2021, getAlaro, WebService
from .textpillow import Text_Image,Text_Infos
from .drawing_obj import Element_To_Draw
[docs]
class genericImagetexture(Element_To_Draw):
"""
Affichage d'une image en OpenGL via une texture
"""
def __init__(self,
which: str,
label: str,
mapviewer,
xmin:float, xmax:float, ymin:float, ymax:float,
imageFile:str = "",
imageObj = None,
transparent_color = None,
tolerance:int = 3,
replace_color = None,
drawing_scale:float = 1.0,
offset:list[float, float] = [0.,0.]) -> None:
""" Initialize the image texture
:param which: Type of image (e.g., 'satellite', 'map', etc.)
:param label: Label for the texture
:param mapviewer: The map viewer object to which this texture belongs
:param xmin: Minimum X coordinate for the texture
:param xmax: Maximum X coordinate for the texture
:param ymin: Minimum Y coordinate for the texture
:param ymax: Maximum Y coordinate for the texture
:param imageFile: Optional file path to load the image from
:param imageObj: Optional PIL Image object to use instead of loading from file
:param transparent_color: Color to treat as transparent in the image
:param tolerance: Tolerance for color matching when replacing transparent color
:param replace_color: Color to replace the transparent color with
:param drawing_scale: Scale factor for the image
:param offset: Offset to apply to the texture position
"""
super().__init__(label, True, mapviewer, False)
try:
self.mapviewer.canvas.SetCurrent(mapviewer.context)
except:
logging.error(_('Opengl setcurrent -- Do you have a active canvas ?'))
return
self.idtexture = (GLuint * 1)()
[docs]
self.idx = 'texture_{}'.format(self.idtexture)
try:
glGenTextures(1, self.idtexture)
except:
raise NameError(
'Opengl glGenTextures -- maybe a conflict with an existing opengl32.dll file - please rename the opengl32.dll in the libs directory and retry')
self.which = which.lower()
self.idx = label
self.name = label
[docs]
self.imageFile = imageFile
self.myImage = imageObj
[docs]
self.drawing_scale = drawing_scale
[docs]
self.offset = offset
if imageFile != "":
if exists(imageFile):
try:
self.myImage = Image.open(imageFile).convert('RGBA')
except Exception as e:
logging.warning(_('Error opening image file : ') + str(imageFile))
logging.info(_('Trying to open image file with increased limit of pixels'))
Image.MAX_IMAGE_PIXELS = 10000000000
self.myImage = Image.open(imageFile).convert('RGBA')
if self.myImage is not None:
self.width = self.myImage.width
self.height = self.myImage.height
if transparent_color is not None:
# replace the transparent color by a fully transparent pixel
colors = np.asarray(self.myImage).copy()
if tolerance == 0:
ij = np.where(colors[:,:,:3] == transparent_color)
else:
ij = np.where(np.isclose(colors[:,:,:3], np.full((colors.shape[0],colors.shape[1],3), transparent_color), atol=tolerance))
# set the alpha channel to 0 for the pixels that are close to the transparent color
colors[ij[0],ij[1],3] = 0
# colorize the pixels that are not transparent
if replace_color is not None:
ij = np.where(colors[:,:,3] > 0)
colors[ij[0],ij[1],:] = replace_color
# create a new image from the modified array
self.myImage = Image.fromarray(colors)
else:
self.width = -99999
self.height = -99999
self.update_minmax()
[docs]
self.oldview = [self.xmin, self.xmax, self.ymin, self.ymax, self.width, self.height, self.time]
[docs]
self.newview = self.oldview.copy()
# Initialize async image loading attributes
[docs]
self._loader = get_global_loader()
[docs]
self._pending_load_request_id: str | None = None
[docs]
self._pending_async_image: Image.Image | None = None
[docs]
self._async_load_bounds: tuple[float, float, float, float] | None = None
self.load()
def __del__(self):
""" Destructor to unload the texture from memory """
self.unload()
[docs]
def unload(self):
""" Unload the texture from memory """
self.mapviewer.canvas.SetCurrent(self.mapviewer.context)
if self.idtexture is not None:
glDeleteTextures(1, self.idtexture)
if self.myImage is not None:
del self.myImage
[docs]
def load(self, imageFile=""):
""" Load the image texture into OpenGL
:param imageFile: Optional file path to load the image from
This method first checks if an asynchronous image has been loaded and is ready.
If so, it uses that image. Otherwise, it proceeds with synchronous loading.
"""
if self.width == -99999 or self.height == -99999:
return
# Check if async loaded image is ready and within current bounds
if self._pending_async_image is not None and self._async_load_bounds is not None:
current_bounds = (self.xmin, self.xmax, self.ymin, self.ymax)
if self._async_load_bounds == current_bounds:
logging.debug(_('Using async-loaded image for ') + self.idx)
self.myImage = self._pending_async_image
self._pending_async_image = None
self._async_load_bounds = None
if self.mapviewer.canvas.SetCurrent(self.mapviewer.context):
mybytes: BytesIO
if imageFile != "":
if not exists(imageFile):
return
self.myImage = Image.open(imageFile).convert('RGBA')
elif self.myImage is None:
return
glBindTexture(GL_TEXTURE_2D, self.idtexture[0])
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, self.myImage.width, self.myImage.height, 0, GL_RGBA,
GL_UNSIGNED_BYTE, self.myImage.tobytes())
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)
glGenerateMipmap(GL_TEXTURE_2D)
else:
raise NameError(
'Opengl setcurrent -- maybe a conflict with an existing opengl32.dll file - please rename the opengl32.dll in the libs directory and retry')
[docs]
def update_minmax(self):
""" Update the spatial extent of the texture based on its size """
if self.myImage is None:
return
dx = self.xmax - self.xmin
dy = self.ymax - self.ymin
scale=dy/dx
if int(scale*4) != int(float(self.height)/float(self.width)*4):
scale = float(self.height)/float(self.width)
self.ymax = self.ymin + dx *scale
[docs]
def reload(self, xmin=-99999, xmax=-99999, ymin=-99999, ymax=-99999):
""" Reload the texture with new spatial bounds and trigger async image update if using cache.
When spatial bounds change, this method:
1. Updates the bounds
2. Checks if the view has changed significantly
3. If using a cache service, schedules async image loading
4. Immediately updates the texture if using direct service (non-cached)
"""
if xmin !=-99999:
self.xmin = xmin
if xmax !=-99999:
self.xmax = xmax
if ymin !=-99999:
self.ymin = ymin
if ymax !=-99999:
self.ymax = ymax
self.update_minmax()
self.newview = [self.xmin, self.xmax, self.ymin, self.ymax, self.width, self.height, self.time]
# Check if view has changed significantly
if self.newview != self.oldview:
# If this texture uses the cache system and has bounds
if hasattr(self, '_cache') and self._cache is not None and hasattr(self, 'category'):
self._schedule_async_image_update()
else:
# Direct load (synchronous) for non-cached services
self.load()
self.oldview = self.newview
[docs]
def _schedule_async_image_update(self):
""" Schedule an asynchronous image update based on current bounds and service.
This is called when bounds change and the texture is using a cached service.
The image is loaded in the background without blocking the UI.
Optimizations:
1. Cancel obsolete requests for this texture (old bounds become irrelevant)
2. Load a degraded image (lower resolution, larger bounds) while waiting for best quality
"""
try:
service = self._get_current_service()
if service is None:
# Fall back to synchronous load if we can't determine service
self.load()
return
# Create a unique request ID based on bounds and service
bounds_key = (self.xmin, self.xmax, self.ymin, self.ymax)
request_id = f"{self.idx}_{service.name}_{id(bounds_key)}"
# OPTIMIZATION 1: Cancel obsolete requests for this texture
# When panning/zooming, old requests become irrelevant and should be cleared
cancelled = self._loader.cancel_obsolete_for_texture(
texture_id=self.idx,
keep_request_id=request_id
)
if cancelled > 0:
logging.debug(_('Cancelled ') + str(cancelled) + _(' obsolete requests for ') + self.idx)
# OPTIMIZATION 2: Load degraded image while waiting for best quality
# Use larger bounds (2x) with lower resolution for immediate display
self._load_degraded_image_for_fallback(service)
# Calculate scale factors for image resolution
sx = self.width / (self.xmax - self.xmin) if (self.xmax - self.xmin) > 0 else 1.0
sy = self.height / (self.ymax - self.ymin) if (self.ymax - self.ymin) > 0 else 1.0
# Create loader function that will be executed in the executor
def load_image_from_service():
return self._cache.get_image_for_async(
service,
self.category,
self.subcategory,
self.xmin, self.xmax, self.ymin, self.ymax,
scale_x=sx, scale_y=sy,
direct_fetch=False
)
# Create callback that will be invoked when image is ready
def on_image_ready(image: Image.Image | None):
if image is not None:
self._pending_async_image = image
self._async_load_bounds = bounds_key
logging.debug(_('Async image ready for ') + self.idx)
else:
logging.warning(_('Failed to load async image for ') + self.idx)
# Schedule the load with the async loader
logging.debug(_('Scheduling async image load for ') + self.idx)
self._loader.schedule_load(
request_id=request_id,
loader_func=load_image_from_service,
callback=on_image_ready,
priority=ImageLoadPriority.NORMAL,
bounds=bounds_key
)
except Exception as e:
logging.error(_('Error scheduling async image load: ') + str(e))
# Fall back to synchronous load on error
self.load()
[docs]
def _load_degraded_image_for_fallback(self, service) -> bool:
""" Load a lower-resolution image at wider bounds for immediate display while best quality loads.
This improves perceived responsiveness by showing a degraded image immediately.
The wider bounds ensure the degraded image covers the viewport during fast pans/zooms.
:param service: WebService enum for the source
:return: True if fallback was loaded, False otherwise
"""
try:
# Use 2x larger bounds (for coverage) with 0.5x resolution (degraded)
margin_x = (self.xmax - self.xmin) * 0.5
margin_y = (self.ymax - self.ymin) * 0.5
fallback_xmin = self.xmin - margin_x
fallback_xmax = self.xmax + margin_x
fallback_ymin = self.ymin - margin_y
fallback_ymax = self.ymax + margin_y
# 0.5x resolution for degraded version (faster to load)
fallback_sx = (self.width * 0.5) / (fallback_xmax - fallback_xmin) if (fallback_xmax - fallback_xmin) > 0 else 1.0
fallback_sy = (self.height * 0.5) / (fallback_ymax - fallback_ymin) if (fallback_ymax - fallback_ymin) > 0 else 1.0
# Try to get fallback image synchronously - if it's in cache it's fast
fallback_image = self._cache.get_image_for_async(
service,
self.category,
self.subcategory,
fallback_xmin, fallback_xmax, fallback_ymin, fallback_ymax,
scale_x=fallback_sx, scale_y=fallback_sy,
direct_fetch=False
)
if fallback_image is not None:
# Store fallback image with different bounds marker
self._pending_async_image = fallback_image
self._async_load_bounds = (fallback_xmin, fallback_xmax, fallback_ymin, fallback_ymax)
logging.debug(_('Loaded degraded fallback image for ') + self.idx)
return True
except Exception as e:
logging.debug(_('Could not load degraded fallback: ') + str(e))
return False
[docs]
def _get_current_service(self) -> WebService | None:
""" Determine the current service based on texture type.
:return: WebService enum value or None
"""
if hasattr(self, 'LifeWatch') and self.LifeWatch:
return WebService.LifeWatch
elif hasattr(self, 'IGN_Belgium') and self.IGN_Belgium:
return WebService.IGN_Belgium
elif hasattr(self, 'Walonmap') and self.Walonmap:
return WebService.Walonmap
elif hasattr(self, 'IGN_France') and self.IGN_France:
return WebService.IGN_France
return None
[docs]
def plot(self, sx=None, sy=None, xmin=None, ymin=None, xmax=None, ymax=None, size=None):
""" alias for paint"""
self.paint()
[docs]
def find_minmax(self,update=False):
"""
Generic function to find min and max spatial extent in data
"""
# Nothing to do, set during initialization phase
pass
[docs]
def uv(self, x: float, y: float) -> tuple[float, float]:
""" Convert coordinates to texture coordinates taking into account the texture's spatial extent,
the scaleing factor, and the offset.
:param x: X coordinate in pixels
:param y: Y coordinate in pixels
:return: Tuple of (u, v) texture coordinates
"""
if self.width == -99999 or self.height == -99999:
return 0.0, 0.0
u = (x - self.offset[0] - self.xmin) / ((self.xmax - self.xmin) * self.drawing_scale)
v = 1.0 - (y - self.offset[1] - self.ymin) / ((self.ymax - self.ymin) * self.drawing_scale)
# Ensure u and v are within the range [0, 1]
u = max(0.0, min(1.0, u))
v = max(0.0, min(1.0, v))
return u, v
[docs]
def paint(self):
""" Paint the image texture on the OpenGL canvas """
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
glColor4f(1., 1., 1., 1.)
glEnable(GL_TEXTURE_2D)
glEnable(GL_BLEND)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
glBindTexture(GL_TEXTURE_2D, self.idtexture[0])
glBegin(GL_QUADS)
# Draw a quad with texture coordinates
xy = [[self.xmin, self.ymax],
[self.xmax, self.ymax],
[self.xmax, self.ymin],
[self.xmin, self.ymin]]
uv = [self.uv(x,y) for x,y in xy]
glTexCoord2f(uv[0][0], uv[0][1])
glVertex2f(xy[0][0], xy[0][1])
glTexCoord2f(uv[1][0], uv[1][1])
glVertex2f(xy[1][0], xy[1][1])
glTexCoord2f(uv[2][0], uv[2][1])
glVertex2f(xy[2][0], xy[2][1])
glTexCoord2f(uv[3][0], uv[3][1])
glVertex2f(xy[3][0], xy[3][1])
# glTexCoord2f(0.0, 0.0)
# glVertex2f(self.xmin, self.ymax)
# glTexCoord2f(1.0, 0.0)
# glVertex2f(self.xmax, self.ymax)
# glTexCoord2f(1.0, 1.0)
# glVertex2f(self.xmax, self.ymin)
# glTexCoord2f(0.0, 1.0)
# glVertex2f(self.xmin, self.ymin)
glEnd()
glDisable(GL_TEXTURE_2D)
glDisable(GL_BLEND)
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
[docs]
class imagetexture(Element_To_Draw):
"""
Affichage d'une image, obtenue depuis un Web service, en OpenGL via une texture
"""
def __init__(self, which: str, label: str, cat: str, subc: str, mapviewer,
xmin:float, xmax:float, ymin:float, ymax:float,
width:int = 1000, height:int = 1000,
France:bool = False, epsg='31370', Vlaanderen:bool = False,
LifeWatch:bool = False, IGN_Belgium:bool = False,
IGN_Cartoweb:bool = False, postFlood2021:bool = False,
Alaro:bool = False) -> None:
super().__init__(label+cat+subc, plotted=False, mapviewer=mapviewer, need_for_wx=False)
try:
mapviewer.canvas.SetCurrent(mapviewer.context)
except:
logging.error(_('Opengl setcurrent -- Do you have a active canvas ?'))
[docs]
self._program = None
[docs]
self._cache = None
self.France = France
[docs]
self.Vlaanderen = Vlaanderen
[docs]
self.LifeWatch = LifeWatch
[docs]
self.IGN_Belgium = IGN_Belgium
[docs]
self.IGN_Cartoweb = IGN_Cartoweb
[docs]
self.postFlood2021 = postFlood2021
[docs]
self.Alaro = Alaro
self.epsg = epsg
if self.France and epsg != 'EPSG:2154':
logging.error(_('Warning : IGN France data is only available in EPSG:2154 -- change your EPSG code'))
return
self.idtexture = (GLuint * 1)()
[docs]
self.idx = 'texture_{}'.format(self.idtexture)
[docs]
self.force_alpha = False
try:
glGenTextures(1, self.idtexture)
except:
raise NameError(
'Opengl glGenTextures -- maybe a conflict with an existing opengl32.dll file - '
'please rename the opengl32.dll in the libs directory and retry')
self.width = width
self.height = height
self.which = which.lower()
self.category = cat # .upper()
self.name = label
self.subcategory = subc # .upper()
[docs]
self.oldview = [self.xmin, self.xmax, self.ymin, self.ymax, self.width, self.height, self.time]
self._init_cache()
self.load()
[docs]
def _init_cache(self):
""" Initialize the cache for this texture based on its category and subcategory """
if self._cache is None:
if self.France:
self._cache = CachedImages(epsg=self.epsg, max_resolution=.25)
elif self.Vlaanderen:
self._cache = CachedImages(epsg=self.epsg, max_resolution=0.5)
elif self.LifeWatch:
self._cache = CachedImages(epsg=self.epsg, max_resolution=2.)
elif self.IGN_Belgium:
self._cache = CachedImages(epsg=self.epsg, max_resolution=0.5)
elif self.IGN_Cartoweb:
self._cache = CachedImages(epsg=self.epsg, max_resolution=0.5)
elif self.postFlood2021:
self._cache = CachedImages(epsg=self.epsg, max_resolution=1.)
elif self.Alaro:
self._cache = CachedImages(epsg=self.epsg, max_resolution=500.)
else:
self._cache = CachedImages(epsg=self.epsg, max_resolution=.5)
[docs]
def load(self):
""" Load the image texture into OpenGL,
fetching it from the appropriate web service if necessary
and caching it for future use.
"""
if self.width == -99999 or self.height == -99999:
logging.debug(_('Invalid image dimensions -- skipping loading texture'))
return
sx = self.mapviewer.sx
sy = self.mapviewer.sy
if self.mapviewer.canvas.SetCurrent(self.mapviewer.context):
mybytes: BytesIO
if self.France:
mybytes = getIGNFrance(self.category, self.epsg,
self.xmin, self.ymin, self.xmax, self.ymax,
self.width, self.height, False)
elif self.Vlaanderen:
mybytes = getVlaanderen(self.category,
self.xmin, self.ymin, self.xmax, self.ymax,
self.width, self.height, False)
elif self.LifeWatch:
mybytes = self._cache.get_image(WebService.LifeWatch, self.category, self.subcategory,
self.xmin, self.xmax, self.ymin, self.ymax,
scale_x= sx, scale_y= sy)
if not mybytes:
logging.warning(_('Error fetching image from cache for : ') + str(self.category + '/' + self.subcategory))
logging.warning(_('Fetching image from web service...'))
mybytes = getLifeWatch(self.category + '_' + self.subcategory,
self.xmin, self.ymin, self.xmax, self.ymax,
self.width, self.height, False)
elif self.IGN_Belgium:
mybytes = self._cache.get_image(WebService.IGN_Belgium, self.category, self.subcategory,
self.xmin, self.xmax, self.ymin, self.ymax,
scale_x= sx, scale_y= sy)
if not mybytes:
logging.warning(_('Error fetching image from cache for : ') + str(self.category + '/' + self.subcategory))
logging.warning(_('Fetching image from web service...'))
mybytes = getNGI(self.subcategory,
self.xmin, self.ymin, self.xmax, self.ymax,
self.width, self.height, False)
elif self.IGN_Cartoweb:
mybytes = getCartoweb(self.subcategory,
self.xmin, self.ymin, self.xmax, self.ymax,
self.width, self.height, False)
elif self.postFlood2021:
mybytes = getOrthoPostFlood2021(self.subcategory,
self.xmin, self.ymin, self.xmax, self.ymax,
self.width, self.height, False)
elif self.Alaro:
mybytes = getAlaro(self.subcategory,
self.xmin, self.ymin, self.xmax, self.ymax,
self.width, self.height, False, time= self.time)
else:
# Try to get tiles from cache (non-blocking approach)
mybytes = self._cache.get_image(WebService.Walonmap,
self.category, self.subcategory,
self.xmin, self.xmax,
self.ymin, self.ymax,
scale_x= sx, scale_y= sy)
if not mybytes:
# No cached tiles - fetch global image quickly
logging.info(_('Fetching global image from web service...'))
mybytes = getWalonmap(self.category + '/' + self.subcategory,
self.xmin, self.ymin, self.xmax, self.ymax,
self.width, self.height, False)
if mybytes is None:
logging.warning(_('Error opening image file : ') + str(self.category + '/' + self.subcategory))
return
try:
if isinstance(mybytes, bytes | BytesIO):
image = Image.open(mybytes)
if image.mode != 'RGBA':
image = image.convert('RGBA')
if self.force_alpha:
if self.alpha < 0.0:
self.alpha = 0.0
if self.alpha > 1.0:
self.alpha = 1.0
alpha = Image.new('L', image.size, int(self.alpha * 255))
image.putalpha(alpha)
elif isinstance(mybytes, str):
image = Image.open(mybytes).convert('RGB')
if image.width != self.width or image.height != self.height:
image = image.resize((self.width, self.height), Image.ANTIALIAS)
image_memory = BytesIO()
image.save(image_memory, format='PNG')
image = Image.open(image_memory)
elif isinstance(mybytes, Image.Image):
image = mybytes
else:
logging.error(_('Unknown type of image file : ') + str(type(mybytes)))
return
except Exception as e:
logging.warning(_('Error opening image file : ') + str(self.category + '/' + self.subcategory))
return
glBindTexture(GL_TEXTURE_2D, self.idtexture[0])
if self.subcategory[:5] == 'ORTHO':
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image.width, image.height, 0, GL_RGBA, GL_UNSIGNED_BYTE,
image.tobytes())
elif image.mode == 'RGB':
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, image.width, image.height, 0, GL_RGB, GL_UNSIGNED_BYTE,
image.tobytes())
else:
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image.width, image.height, 0, GL_RGBA, GL_UNSIGNED_BYTE,
image.tobytes())
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)
glGenerateMipmap(GL_TEXTURE_2D)
else:
raise NameError(
'Opengl setcurrent -- maybe a conflict with an existing opengl32.dll file - '
'please rename the opengl32.dll in the libs directory and retry')
[docs]
def _update_texture_from_image(self, image):
"""Update the OpenGL texture with a new image (used for progressive tile updates)."""
try:
self.mapviewer.canvas.SetCurrent(self.mapviewer.context)
glBindTexture(GL_TEXTURE_2D, self.idtexture[0])
if isinstance(image, Image.Image):
img = image
if img.mode != 'RGBA':
img = img.convert('RGBA')
else:
return
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, img.width, img.height, 0, GL_RGBA, GL_UNSIGNED_BYTE,
img.tobytes())
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)
glGenerateMipmap(GL_TEXTURE_2D)
logging.info(_('Texture updated with precise tiles'))
except Exception as e:
logging.warning(_('Error updating texture: ') + str(e))
[docs]
def reload(self):
dx = self.mapviewer.xmax - self.mapviewer.xmin
dy = self.mapviewer.ymax - self.mapviewer.ymin
cx = self.mapviewer.mousex
cy = self.mapviewer.mousey
coeff = .5
self.xmin = cx - dx * coeff
self.xmax = cx + dx * coeff
self.ymin = cy - dy * coeff
self.ymax = cy + dy * coeff
self.width = int(self.mapviewer.canvaswidth * 2 * coeff)
self.height = int(self.mapviewer.canvasheight * 2 * coeff)
self.newview = [self.xmin, self.xmax, self.ymin, self.ymax, self.width, self.height, self.time]
if self.newview != self.oldview:
self.load()
self.oldview = self.newview
[docs]
def plot(self, sx=None, sy=None, xmin=None, ymin=None, xmax=None, ymax=None, size=None):
""" alias for paint"""
self.paint(sx=sx, sy=sy, xmin=xmin, ymin=ymin, xmax=xmax, ymax=ymax)
[docs]
def find_minmax(self,update=False):
"""
Generic function to find min and max spatial extent in data
"""
# Nothing to do, set during initialization phase
pass
[docs]
def init_shader(self):
# Compile The Program and shaders
_vertex_shader = glCreateShader(GL_VERTEX_SHADER)
with open(Path(__file__).parent / "shaders/texture_vertex_shader.glsl") as file:
VERTEX_SHADER = file.read()
glShaderSource(_vertex_shader, VERTEX_SHADER)
glCompileShader(_vertex_shader)
if glGetShaderiv(_vertex_shader, GL_COMPILE_STATUS, None) == GL_FALSE:
info_log = glGetShaderInfoLog(_vertex_shader)
print(info_log)
glDeleteProgram(_vertex_shader)
raise Exception("Can't compile shader on GPU")
_fragment_shader = glCreateShader(GL_FRAGMENT_SHADER)
with open(Path(__file__).parent / "shaders/texture_fragment_shader.glsl") as file:
FRAGMENT_SHADER = file.read()
glShaderSource(_fragment_shader, FRAGMENT_SHADER)
glCompileShader(_fragment_shader)
if glGetShaderiv(_fragment_shader, GL_COMPILE_STATUS, None) == GL_FALSE:
info_log = glGetShaderInfoLog(_fragment_shader)
print(info_log)
glDeleteProgram(_fragment_shader)
raise Exception("Can't compile shader on GPU")
self._program = glCreateProgram()
glAttachShader(self._program, _vertex_shader)
glAttachShader(self._program, _fragment_shader)
# glAttachShader(self._program, _geometry_shader)
glLinkProgram(self._program)
# Check if the program is compiled
if glGetProgramiv(self._program, GL_LINK_STATUS) == GL_FALSE:
info_log = glGetProgramInfoLog(self._program)
print(info_log)
raise Exception("Can't link shader program")
glDeleteShader(_vertex_shader)
glDeleteShader(_fragment_shader)
# glDeleteShader(_geometry_shader)
# xmin, xmax, ymin, ymax = tes valeurs
vertices = np.array([ # x y u v
self.xmin, self.ymax, 0., 0.,
self.xmax, self.ymax, 1., 0.,
self.xmin, self.ymin, 0., 1.,
self.xmax, self.ymin, 1., 1.,
self.xmax, self.ymax, 1., 0.,
], dtype=np.float32)
self._vao = glGenVertexArrays(1)
self._vbo = glGenBuffers(1)
glBindVertexArray(self._vao)
glBindBuffer(GL_ARRAY_BUFFER, self._vbo)
glBufferData(GL_ARRAY_BUFFER, vertices.nbytes, vertices, GL_STATIC_DRAW)
# position (location = 0)
glEnableVertexAttribArray(0)
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * 4, ctypes.c_void_p(0))
# uv (location = 1)
glEnableVertexAttribArray(1)
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * 4, ctypes.c_void_p(8))
glBindVertexArray(0)
[docs]
def paint(self, sx=None, sy=None, xmin=None, ymin=None, xmax=None, ymax=None):
if self._program is None:
self.init_shader()
# xmin, xmax, ymin, ymax = tes valeurs
vertices = np.array([ # x y u v
self.xmin, self.ymax, 0., 0.,
self.xmax, self.ymax, 1., 0.,
self.xmin, self.ymin, 0., 1.,
self.xmax, self.ymin, 1., 1.,
self.xmax, self.ymax, 1., 0.,
], dtype=np.float32)
glUseProgram(self._program)
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
_mvpLoc = glGetUniformLocation(self._program, "mvp")
glUniformMatrix4fv(_mvpLoc, 1, GL_FALSE, self.mapviewer.mvp)
glActiveTexture(GL_TEXTURE0)
glBindTexture(GL_TEXTURE_2D, self.idtexture[0])
glUniform1i(glGetUniformLocation(self._program, "tex"), 0)
glEnable(GL_BLEND)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
glBindVertexArray(self._vao)
glBindBuffer(GL_ARRAY_BUFFER, self._vbo)
glBufferSubData(GL_ARRAY_BUFFER, 0, vertices.nbytes, vertices)
glBindBuffer(GL_ARRAY_BUFFER, 0)
glDrawArrays(GL_TRIANGLE_FAN, 0, 5)
glBindVertexArray(0)
glDisable(GL_BLEND)
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
glUseProgram(0)
[docs]
def check_plot(self):
self.plotted = True
[docs]
def uncheck_plot(self, unload=True):
self.plotted = False
[docs]
class Text_Image_Texture(genericImagetexture):
def __init__(self, text: str, mapviewer, proptext:Text_Infos, vector, x:float, y:float) -> None:
"""Gestion d'un texte sous forme de texture OpenGL
Args:
text (str): texte à afficher
mapviewer (wolf_mapviewer): objet parent sur lequel dessiner
proptext (Text_Infos): infos sur la mise en forme
vector (vector): vecteur associé au texte
x (float): point d'accroche X
y (float): point d'accroche Y
"""
[docs]
self.vector = vector
[docs]
self.proptext = proptext
[docs]
self.mapviewer = mapviewer
self.findscale()
self.proptext.findsize(text)
xmin, xmax, ymin, ymax = proptext.getminmax(self.x,self.y)
super().__init__('other', text, mapviewer, xmin, xmax, ymin, ymax)
if self.myImage is not None:
self.width = self.myImage.width
self.height = self.myImage.height
[docs]
self.oldview = [self.xmin, self.xmax, self.ymin, self.ymax, self.width, self.height, self.time]
[docs]
def findscale(self):
self.proptext.setscale(self.mapviewer.sx, self.mapviewer.sy)
[docs]
def load(self, imageFile=""):
if self.mapviewer.SetCurrentContext():
if imageFile != "":
if not exists(imageFile):
return
self.myImage = Image.open(imageFile).convert('RGBA')
else:
self.myImage = Text_Image(self.name, self.proptext).image
if self.myImage is None:
return
glEnable(GL_TEXTURE_2D)
glPixelStorei(GL_UNPACK_ALIGNMENT, 1)
glBindTexture(GL_TEXTURE_2D, self.idtexture[0])
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, self.myImage.width, self.myImage.height, 0, GL_RGBA,
GL_UNSIGNED_BYTE, self.myImage.transpose(Image.FLIP_TOP_BOTTOM).tobytes())
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)
glGenerateMipmap(GL_TEXTURE_2D)
glBindTexture(GL_TEXTURE_2D, 0)
glDisable(GL_TEXTURE_2D)
else:
raise NameError(
'Opengl setcurrent -- maybe a conflict with an existing opengl32.dll file - please rename the opengl32.dll in the libs directory and retry')
[docs]
def paint(self):
self.findscale()
self.proptext.setsize_real()
if self.proptext.adapt_fontsize(self.name):
self.update_image()
x,y = self.proptext.getcorners(self.x,self.y)
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
glColor4f(1., 1., 1., 1.)
glEnable(GL_TEXTURE_2D)
glEnable(GL_BLEND)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
# glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA)
glBindTexture(GL_TEXTURE_2D, self.idtexture[0])
glBegin(GL_QUADS)
glTexCoord2f(0.0, 0.0)
glVertex2f(x[0], y[0])
glTexCoord2f(1.0, 0.0)
glVertex2f(x[1], y[1])
glTexCoord2f(1.0, 1.0)
glVertex2f(x[2], y[2])
glTexCoord2f(0.0, 1.0)
glVertex2f(x[3], y[3])
glEnd()
glBindTexture(GL_TEXTURE_2D, 0)
glDisable(GL_TEXTURE_2D)
glDisable(GL_BLEND)
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
[docs]
def update_image(self, newtext:str="", proptext:Text_Infos=None):
if newtext !="":
self.name = newtext
if proptext is not None:
self.proptext = proptext
self.load()