TomSchimansky / TkinterMapView

A python Tkinter widget to display tile based maps like OpenStreetMap or Google Satellite Images.
Creative Commons Zero v1.0 Universal
631 stars 86 forks source link

Unable to load overlay layer using "set_overlay_tile_server" #117

Closed hanselmoovita closed 11 months ago

hanselmoovita commented 12 months ago

Example used: map_with_customtkinter.py

I added a 4th option to include overlay option for "Railway". Modifications made were:

In self.map_option_menu:

self.map_option_menu = customtkinter.CTkOptionMenu(self.frame_left, values=["OpenStreetMap", "Google normal", "Google satellite", "railway"]

In def change map:

elif new_map == "railway": self.map_widget.set_overlay_tile_server("http://a.tiles.openrailwaymap.org/standard/{z}/{x}/{y}.png") # railway infrastructure

However, upon running the program and selecting the "railway" option for Tile Server, the TkinterMapView returned blank map:

Tkintermapview

I used the suggested API link given under "Use other tile servers" in your Readme.

May I ask for help to fix or advice on this issue

LorenzoMattia commented 11 months ago

Hi @hanselmoovita sorry for late answer. You are probably having an exception raising in the try block at line 494 of the map_widget.py file that results in one of the except blocks which is returning self.empty_tile_image. Could you please provide further details?

hanselmoovita commented 11 months ago

@LorenzoMattia

Can I clarify with you, if I would like to apply overlay layer on top of Main Tile layer, is this the correct way to do it? See screenshot below, right window for highlighted portion:

Screenshot from 2023-12-18 14-13-57

The current problem I faced is that when I added this line, the tkinter application goes blank when I made the selection with overlay (ie: Google satellite)

For this scenario, I have not made modifications to map_widget.py I have tried to modify the definition for but have not been successful either

For your kind advice please

LorenzoMattia commented 11 months ago

@hanselmoovita

Yes, you are doing right in setting the overlay_tile_server.

The problem I think you are facing is in the block I mentioned in my previous answer.

I suggest you to execute the code in debug mode or trivially to temporarily remove the try except block at line 494 in map_widget.py to see what exception is raised. My intuition is that a certain problem is causing that block to fall in on of the except blocks in which the empty_tile_image is being returned and that is probably the blank image you are seeing in the screen.

I'm talking about this piece of code

        try:
            url = self.tile_server.replace("{x}", str(x)).replace("{y}", str(y)).replace("{z}", str(zoom))
            image = Image.open(requests.get(url, stream=True, headers={"User-Agent": "TkinterMapView"}).raw)

            if self.overlay_tile_server is not None:
                url = self.overlay_tile_server.replace("{x}", str(x)).replace("{y}", str(y)).replace("{z}", str(zoom))
                image_overlay = Image.open(requests.get(url, stream=True, headers={"User-Agent": "TkinterMapView"}).raw)
                image = image.convert("RGBA")
                image_overlay = image_overlay.convert("RGBA")

                if image_overlay.size is not (self.tile_size, self.tile_size):
                    image_overlay = image_overlay.resize((self.tile_size, self.tile_size), Image.ANTIALIAS)

                image.paste(image_overlay, (0, 0), image_overlay)

            if self.running:
                image_tk = ImageTk.PhotoImage(image)
            else:
                return self.empty_tile_image

            self.tile_image_cache[f"{zoom}{x}{y}"] = image_tk
            return image_tk

        except PIL.UnidentifiedImageError:  # image does not exist for given coordinates
            self.tile_image_cache[f"{zoom}{x}{y}"] = self.empty_tile_image
            return self.empty_tile_image

        except requests.exceptions.ConnectionError:
            return self.empty_tile_image

        except Exception:
            return self.empty_tile_image

Keep me updated once you have tried!

hanselmoovita commented 11 months ago

I've removed the entire try-except code block starting from line 494 in map_widget.py, and when the app is launched, the default selection OpenStreetMap shows a blank grey screen, and many exception errors were returned in the debugger in one go:

Screenshot from 2023-12-19 09-08-51

No other modifications were made other than the one you've suggested above. The full log of exception errors are found below:

Error log.txt

LorenzoMattia commented 11 months ago

Hi @hanselmoovita, sorry, I did not explain myself well.

You do not have to completely remove the try catch block, but only to remove the try, leaving the block of code inside it, and all the catches. So this code:

        try:
            url = self.tile_server.replace("{x}", str(x)).replace("{y}", str(y)).replace("{z}", str(zoom))
            image = Image.open(requests.get(url, stream=True, headers={"User-Agent": "TkinterMapView"}).raw)

            if self.overlay_tile_server is not None:
                url = self.overlay_tile_server.replace("{x}", str(x)).replace("{y}", str(y)).replace("{z}", str(zoom))
                image_overlay = Image.open(requests.get(url, stream=True, headers={"User-Agent": "TkinterMapView"}).raw)
                image = image.convert("RGBA")
                image_overlay = image_overlay.convert("RGBA")

                if image_overlay.size is not (self.tile_size, self.tile_size):
                    image_overlay = image_overlay.resize((self.tile_size, self.tile_size), Image.ANTIALIAS)

                image.paste(image_overlay, (0, 0), image_overlay)

            if self.running:
                image_tk = ImageTk.PhotoImage(image)
            else:
                return self.empty_tile_image

            self.tile_image_cache[f"{zoom}{x}{y}"] = image_tk
            return image_tk

        except PIL.UnidentifiedImageError:  # image does not exist for given coordinates
            self.tile_image_cache[f"{zoom}{x}{y}"] = self.empty_tile_image
            return self.empty_tile_image

        except requests.exceptions.ConnectionError:
            return self.empty_tile_image

        except Exception:
            return self.empty_tile_image

will be:

url = self.tile_server.replace("{x}", str(x)).replace("{y}", str(y)).replace("{z}", str(zoom))
image = Image.open(requests.get(url, stream=True, headers={"User-Agent": "TkinterMapView"}).raw)

if self.overlay_tile_server is not None:
    url = self.overlay_tile_server.replace("{x}", str(x)).replace("{y}", str(y)).replace("{z}", str(zoom))
    image_overlay = Image.open(requests.get(url, stream=True, headers={"User-Agent": "TkinterMapView"}).raw)
    image = image.convert("RGBA")
    image_overlay = image_overlay.convert("RGBA")

    if image_overlay.size is not (self.tile_size, self.tile_size):
         image_overlay = image_overlay.resize((self.tile_size, self.tile_size), Image.ANTIALIAS)

    image.paste(image_overlay, (0, 0), image_overlay)

if self.running:
     image_tk = ImageTk.PhotoImage(image)
else:
     return self.empty_tile_image

 self.tile_image_cache[f"{zoom}{x}{y}"] = image_tk
return image_tk

Obviously, this operation only aimes at understanding the error that is causing the display of blank images, indeed using the try catch, the error is never shown in the console. Morever, even if this approach works, the best way to do this would be to use the debugger and not to modify the code. Once you will understand why that part of code is generating an exception you will probably closer to the solution.

Let me know!

hanselmoovita commented 11 months ago

Hi,

The change is able to work based on the modification to if-else conditional as you have suggested:

Screenshot from 2023-12-21 10-12-59

For the above test, I used OpenStreetMap as base layer with Railway lines as overlay.

Please note that the Antialias class seems to have been removed, I have replaced it with LANCZOS as per: https://stackoverflow.com/questions/76616042/attributeerror-module-pil-image-has-no-attribute-antialias

Do note that there are warning issued:

/usr/lib/python3/dist-packages/requests/init.py:89: RequestsDependencyWarning: urllib3 (2.1.0) or chardet (3.0.4) doesn't match a supported version! warnings.warn("urllib3 ({}) or chardet ({}) doesn't match a supported " Exception in thread Thread-1: Traceback (most recent call last): File "/usr/lib/python3.8/threading.py", line 932, in _bootstrap_inner self.run() File "/usr/lib/python3.8/threading.py", line 870, in run self._target(*self._args, **self._kwargs) File "/home/han/Documents/TkinterMapView-MY/src/map_widget.py", line 436, in pre_cache self.request_image(zoom, x, self.pre_cache_position[1] + radius, db_cursor=db_cursor) File "/home/han/Documents/TkinterMapView-MY/src/map_widget.py", line 495, in request_image image = Image.open(requests.get(url, stream=True, headers={"User-Agent": "TkinterMapView"}).raw) File "/home/han/.local/lib/python3.8/site-packages/PIL/Image.py", line 3305, in open raise UnidentifiedImageError(msg) PIL.UnidentifiedImageError: cannot identify image file <_io.BytesIO object at 0x7f8be06630e0>

hanselmoovita commented 11 months ago

However, can I also ask, if Tkintermapview is able to support Openstreetmap/Google maps as base layer, with local map tiles as overlay layer?

With the latest update to implementation, only the map tile layer (overlay) is loaded, and the base layer with Openstreetmap doesn't load:

Conditions to which this issue is attempted:

  1. Replacing try-except loop with if-else loop to the map_widget as you have suggested
  2. On local map tile folder, terminal was started and I entered <python3 -m http.server 8080> so as to be able to retrieve local map tile images
  3. The local map tiles have coordinates pre-marked:

Screenshot from 2023-12-21 10-18-32

The intended implementation is to look like this:

Screenshot from 2023-12-21 10-22-35

How do you suggest map_widget.py maybe further modified to support this implementation?

LorenzoMattia commented 11 months ago

Hi @hanselmoovita, cool! Removing the try revealed that the error that was causing the blank image to be returned in one of those catch blocks was related to that Image.LANCZOS.

So now you can actually reintroduce the try and bring back the code to its original version, obviously keeping the fix of the LANCZOS. I would suggest this to you, because using try catches will prevent the program to crash when, for instance, the server from which you download the images is not available.

Concerning your second question, this is surely possible, but it is necessary to change something in the request_image method. Are you using a db created with the offline_loading.py file to load offline the overlays?

hanselmoovita commented 11 months ago

Hi @LorenzoMattia Yes, if I revert the code back to its original version, and keeping the fix for LANCZOS, the crash issue disappears.

No....I did not create db using offline_loading.py; I set up a local HTTP server by opening an Ubuntu terminal, screenshot shown below. 0 to 19 indicates the zoom level:

Screenshot from 2023-12-22 20-28-58

While I am able to load the local tiles using set_tile_server as the sole layer, I am not able to display the local tiles if it is an overlay layer, with OSM as base layer with the original mplementation (only change applied is changing line 504 ANTIALIAS-->LANCZOS):

Screenshot from 2023-12-22 20-39-56

Would like to ask how def set_overlay_tile_server (or any other related functions) can be modified to support image tile overlay.....

LorenzoMattia commented 11 months ago

Hi @hanselmoovita, first of all I want to clarify that in my answer I am supposing your self.database_path to be unspecified (None).

Starting from this assumption, what I can tell you is that problems are probably again in that try catch block. So, I would suggest you again to remove that try, as you already did to discover the LANCZOS problem, and see what is the error that is causing this problem. Indeed, that part of code should actually work, and the behavior you are obtaining is really strange.

Lastly, just to be sure, are your overlay images .png with no bacground?

Keep me updated!

hanselmoovita commented 11 months ago

Hi @LorenzoMattia I have once again attempted to replace the try-except block with the if-else block at Line 494 which you have suggested.

For this scenario, selecting option with the OSM base layer + overlay doesn't work either. Additionally, I noticed the following issues:

  1. Multiple traceback errors of this type returned: AttributeError: 'PhotoImage' object has no attribute '_PhotoImage__photo' Exception ignored in: <function PhotoImage.del at 0x7fc227a2d280> Traceback (most recent call last): File "/home/han/.local/lib/python3.8/site-packages/PIL/ImageTk.py", line 132, in del

  2. Location would default to Berlin, even though I have specified self.map_widget.set_address to Singapore

  3. The default location would go into the highest resolution; sometimes, when I attempted to change the map selection to Google, a black tile appears instead:

Scenario illustrated in screenshot below:

Screenshot from 2023-12-25 17-40-52

The overlay_images.png have no background; the background intended to be used would be either OSM or Google streetmap. If you would like to have a copy of the overlay images, you can email me at hansel.chan@moovita.com

Hope to hear from you again, wishing you a Merry Christmas!

LorenzoMattia commented 10 months ago

Hi @hanselmoovita, thank you and I hope you had a really Merry Christmas!

Answering to your points:

  1. I have already seen those "Exception ignored" in my application. In my case, they happened sometimes when the tiles loading was running while the application is not already completely initialized. Meaning that your init() function is not ended when the tiles are loading. However, the not loaded tiles referred by those exceptions were then loaded zooming in/out on the map.
  2. Concerning the default location, I would suggest you to look for the (latitude, longitude) coordinates on Google maps or wherever you prefer and use them in the set_position function instead of using the set_address, it can fail sometimes. However, if you prefer to use set_address pay attention to the fact that at line 153 the map sets its position to Berlin, so note that yout set_address should be done before it.
  3. I actually do not know, I should inspect your code for this.

Hoping to be helpful, let me know!

hanselmoovita commented 10 months ago

@LorenzoMattia Good day, I am happy to share my code with you, is it possible you send me a private message at hansel.chan@moovita.com so that I can share it with you?

Wishing you a Happy New Year