Source code for wolfhece.wolf_texture

"""
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 """
[docs] name: str
[docs] idtexture: int
[docs] width: int
[docs] height: int
[docs] which: str
[docs] myImage: Image
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
[docs] self.time = None
[docs] self.xmin = xmin
[docs] self.xmax = xmax
[docs] self.ymin = ymin
[docs] self.ymax = ymax
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 """
[docs] name: str
[docs] idtexture: int
[docs] width: int
[docs] height: int
[docs] which: str
[docs] category: str
[docs] subcategory: str
[docs] France: bool
[docs] epsg: str
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
[docs] self.xmin = xmin
[docs] self.xmax = xmax
[docs] self.ymin = ymin
[docs] self.ymax = ymax
self.idtexture = (GLuint * 1)()
[docs] self.idx = 'texture_{}'.format(self.idtexture)
[docs] self.time = None
[docs] self.alpha = 1.0
[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.x = x
[docs] self.y = 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()