From: Marcus Date: Thu, 12 Sep 2024 20:51:39 +0000 (+0200) Subject: Commit after switch to Linux - no noteworthy change in code X-Git-Url: https://marstr.online/code/gitweb.cgi?a=commitdiff_plain;h=85352308be014a69e678307c25605a69cc1790b7;p=marstr%2Forthographic.git Commit after switch to Linux - no noteworthy change in code --- diff --git a/defines.py b/defines.py index 11c56e0..64e9481 100644 --- a/defines.py +++ b/defines.py @@ -1,267 +1,267 @@ - -# ------------------------------------------------------------------- -# ORTHOGRAPHIC -# Your personal aerial satellite. Always on. At any altitude.* -# Developed by MarStrMind -# License: Open Software License 3.0 -# Up to date version always on marstr.online -# ------------------------------------------------------------------- -# defines.py -# Variables we need in all functions. Each one is documented below. -# ------------------------------------------------------------------- - -# Your data folder - meaning where the cache is stored, and where the finished -# tiles will go. This is also the folder where the image generation sources are -# stored. -mstr_datafolder = "M:/Developer/Projects/orthographic/" - -# API endpoint to acquire OSM data (bonus: I have my own) -mstr_osm_endpoint = "https://marstr.online/osm/v1/" - -# Define the texture resolution you want to have your photos at. -mstr_photores = 2048 # <- Change this to 4096 for 16k resolution - -# Radius of zoom level 18 aerials around airports with ICAO code -# Value is in tiles - not in km -# -# Value = 5: -# ##### -# ##### -# ##X## -# ##### -# ##### -# -# The "X" is the tile with the airport -mstr_airport_radius = 5 - -# Clear cache after generating a complete tile? -mstr_clear_cache = True -# Removes the masks etc after a tile has been generated. Strong recommendation to set this -# to True. - -# Whether or not you want to see progress of the tool as it walks on. -# High recommendation to leave this on. -mstr_show_log = True - - -# Should a pseudo shadow be rendered on certain elements? -# The sun is usually somewhere when a photo is taken during the day. Therefore, -# some kind of shadow is cast in a direction, but this depends on a lot of factors. -# We will simply things to achieve good-looking results. -# You can, however, disable the shadow rendering layer here. -mstr_shadow_enabled = True -mstr_shadow_strength = 0.85 -mstr_shadow_shift = 24 -# The tags that cast shadows -mstr_shadow_casters = [ - ("landuse", "forest"), - ("leisure", "nature_reserve"), - ("building", "semidetached_house"), - ("building", "apartments"), - ("building", "garage"), - ("building", "office"), - ("building", "retail"), - ("building", "industrial"), - ("building", "yes") -] - - -# Whether or not to generate X-Plane Scenery files -mstr_xp_genscenery = True - -# X-Plane specific -mstr_xp_dsftool = "M:/Developer/Projects/orthographic/bin/DSFTool.exe" -mstr_xp_folder = "M:/Flight Sim/Simulator/11/" -# Whether or not you want normal maps for certain geographical elements -# to make them appear more realistic. -mstr_xp_generate_normal_maps = True - -# If you set the above to true, you can -# 1) define for which features to generate normal maps for, and -# 2) the specularity for each feature. -# A specularity value of 100 is fully specular, useful for water -# A specularity value of 10 is more useful for inland features. -# Sand may also reflect some of its surface, so possibly 25 is good there. -mstr_xp_normal_maps = [ - ("landuse", "farmland", 10), - ("landuse", "forest", 10), - ("leisure", "nature_reserve", 10), - ("natural", "water", 100), - ("water", "pond", 100), - ("water", "river", 100), - ("water", "lake", 100) -] - -# How much of a tile we need for each zoom level. The higher -# the zoom level, the smaller the area to generate a mask of - but also -# higher detail. -mstr_zl_16 = 0.08 -mstr_zl_17 = 0.048 -mstr_zl_18 = 0.016 -mstr_zl_19 = 0.008 - - -# The layers we will process, from bottom to top. -# Tag, and value. -# Strong recommendation NOT to alter this list - -# generating the orthos depends on the pool of source -# material I provide for these layers. If you add your own -# OSM tag, and there is no source material, the script will -# most likely crash. -mstr_ortho_layers = [ - # Z-Order 0 - ("landuse", "residential", "landuse", "residential-boundary"), - ("boundary", "administrative", "admin_level", ["8", "9", "10", "12"], "landuse", "residential-boundary"), - # Z-Order 1 - ("landuse", "grass", "landuse", "grass"), - ("landuse", "cemetery", "landuse", "grass"), - ("landuse", "greenfield", "landuse", "grass"), - ("landuse", "orchard", "landuse", "meadow"), - ("landuse", "meadow", "landuse", "meadow"), - ("landuse", "recreation_ground", "landuse", "meadow"), - ("leisure", "golf_course", "leisure", "golf_course"), - ("barrier", "hedge", "natural", "heath"), - ("landuse", "vineyard", "landuse", "meadow"), - ("highway", "track", 3), - ("highway", "path", 3), - ("natural", "bare_rock", "natural", "bare_rock"), - ("natural", "grassland", "landuse", "meadow"), - ("leisure", "park", "leisure", "green"), - ("leisure", "dog_park", "leisure", "green"), - ("leisure", "garden", "leisure", "green"), - ("leisure", "sports_centre", "leisure", "green"), - ("leisure", "pitch", "leisure", "green"), - ("landuse", "farmland", "landuse", "farmland"), - ("landuse", "farmyard", "landuse", "farmland"), - ("landuse", "military", "landuse", "residential-boundary"), - # Z-Order 2 - ("highway", "service", 6), - ("highway", "residential", 12), - ("highway", "footway", 4), - ("highway", "primary", 25), - ("highway", "secondary", 13), - ("highway", "tertiary", 20), - ("highway", "unclassified", 17), - ("highway", "living_street", 12), - ("waterway", "river", 10), - ("waterway", "stream", 10), - ("leisure", "nature_reserve", "landuse", "forest"), - ("landuse", "forest", "landuse", "forest"), - ("natural", "wetland", "natural", "wetland"), - ("natural", "scrub", "natural", "scrub"), - ("natural", "heath", "natural", "heath"), - ("natural", "sand", "natural", "sand"), - ("natural", "desert", "natural", "desert"), - # Z-Order 3 - ("natural", "water", "natural", "water"), - ("natural", "bay", "natural", "beach"), - ("natural", "beach", "natural", "beach"), - ("water", "lake", "natural", "water"), - ("water", "pond", "natural", "water"), - ("water", "river", "natural", "water"), - ("amenity", "parking", "amenities", "parking"), - ("highway", "pedestrian", 4), - # Z-Order 4 - ("highway", "motorway", 32), - ("railway", "rail", 5), - # Z-Order 5 - ("aeroway", "taxiway", 42), - ("aeroway", "runway", 80), - ("building", "detached", "building", "common"), - ("building", "church", "building", "common"), - ("building", "hotel", "building", "industrial"), - ("building", "farm", "building", "industrial"), - ("building", "semidetached_house", "building", "common"), - ("building", "apartments", "building", "common"), - ("building", "civic", "building", "common"), - ("building", "garage", "building", "industrial"), - ("building", "office", "building", "office"), - ("building", "retail", "building", "industrial"), - ("building", "industrial", "building", "industrial"), - ("building", "house", "building", "house"), - ("building", "terrace", "building", "industrial"), - ("building", "hangar", "building", "industrial"), - ("building", "school", "building", "common"), - ("building", "yes", "building", "common"), - ("place", "sea", "natural", "sea"), - ("place", "ocean", "natural", "sea") -] - - -# Blur values for the single masks of the ortho layers -mstr_mask_blur = [ - # Z-Order 0 - ("landuse", "residential", 30), - ("boundary", "administrative", 30), - # Z-Order 1 - ("landuse", "grass", 30), - ("landuse", "cemetery", 30), - ("landuse", "greenfield", 30), - ("landuse", "orchard", 30), - ("landuse", "meadow", 30), - ("barrier", "hedge", 5), - ("landuse", "recreation_ground", 20), - ("landuse", "vineyard", 30), - ("natural", "grassland", 30), - ("natural", "wetland", 30), - ("natural", "scrub", 20), - ("natural", "heath", 20), - ("leisure", "park", 30), - ("leisure", "golf_course", 25), - ("leisure", "dog_park", 35), - ("leisure", "garden", 20), - ("leisure", "sports_centre", 5), - ("leisure", "pitch", 2), - ("landuse", "farmland", 10), - ("landuse", "farmyard", 10), - # Z-Order 2 - ("landuse", "forest", 20), - ("leisure", "nature_reserve", 20), - ("landuse", "military", 30), - # Z-Order 3 - ("natural", "bare_rock", 25), - ("natural", "water", 4), - ("natural", "bay", 30), - ("natural", "beach", 30), - ("water", "lake", 10), - ("water", "pond", 10), - ("water", "river", 10), - ("waterway", "river", 10), - ("waterway", "stream", 10), - ("amenity", "parking", 3), - ("highway", "pedestrian", 12), - # Z-Order 4 - ("highway", "motorway", 5), - ("highway", "primary", 5), - ("highway", "secondary", 5), - ("highway", "tertiary", 5), - ("highway", "unclassified", 5), - ("highway", "living_street", 5), - ("highway", "residential", 5), - ("highway", "service", 3), - ("highway", "footway", 3), - ("highway", "track", 3), - ("highway", "path", 3), - ("railway", "rail", 4), - # Z-Order 5 - ("aeroway", "taxiway", 12), - ("aeroway", "runway", 12), - ("building", "detached", 1), - ("building", "church", 1), - ("building", "hotel", 1), - ("building", "farm", 1), - ("building", "semidetached_house", 1), - ("building", "apartments", 1), - ("building", "civic", 1), - ("building", "garage", 1), - ("building", "office", 1), - ("building", "retail", 1), - ("building", "industrial", 1), - ("building", "house", 1), - ("building", "terrace", 1), - ("building", "hangar", 1), - ("building", "school", 1), - ("building", "yes", 1), - ("place", "sea", 1), - ("place", "ocean", 1) -] + +# ------------------------------------------------------------------- +# ORTHOGRAPHIC +# Your personal aerial satellite. Always on. At any altitude.* +# Developed by MarStrMind +# License: Open Software License 3.0 +# Up to date version always on marstr.online +# ------------------------------------------------------------------- +# defines.py +# Variables we need in all functions. Each one is documented below. +# ------------------------------------------------------------------- + +# Your data folder - meaning where the cache is stored, and where the finished +# tiles will go. This is also the folder where the image generation sources are +# stored. +mstr_datafolder = "M:/Developer/Projects/orthographic/" + +# API endpoint to acquire OSM data (bonus: I have my own) +mstr_osm_endpoint = "https://marstr.online/osm/v1/" + +# Define the texture resolution you want to have your photos at. +mstr_photores = 2048 # <- Change this to 4096 for 16k resolution + +# Radius of zoom level 18 aerials around airports with ICAO code +# Value is in tiles - not in km +# +# Value = 5: +# ##### +# ##### +# ##X## +# ##### +# ##### +# +# The "X" is the tile with the airport +mstr_airport_radius = 5 + +# Clear cache after generating a complete tile? +mstr_clear_cache = True +# Removes the masks etc after a tile has been generated. Strong recommendation to set this +# to True. + +# Whether or not you want to see progress of the tool as it walks on. +# High recommendation to leave this on. +mstr_show_log = True + + +# Should a pseudo shadow be rendered on certain elements? +# The sun is usually somewhere when a photo is taken during the day. Therefore, +# some kind of shadow is cast in a direction, but this depends on a lot of factors. +# We will simply things to achieve good-looking results. +# You can, however, disable the shadow rendering layer here. +mstr_shadow_enabled = True +mstr_shadow_strength = 0.85 +mstr_shadow_shift = 24 +# The tags that cast shadows +mstr_shadow_casters = [ + ("landuse", "forest"), + ("leisure", "nature_reserve"), + ("building", "semidetached_house"), + ("building", "apartments"), + ("building", "garage"), + ("building", "office"), + ("building", "retail"), + ("building", "industrial"), + ("building", "yes") +] + + +# Whether or not to generate X-Plane Scenery files +mstr_xp_genscenery = True + +# X-Plane specific +mstr_xp_dsftool = "M:/Developer/Projects/orthographic/bin/DSFTool.exe" +mstr_xp_folder = "M:/Flight Sim/Simulator/11/" +# Whether or not you want normal maps for certain geographical elements +# to make them appear more realistic. +mstr_xp_generate_normal_maps = True + +# If you set the above to true, you can +# 1) define for which features to generate normal maps for, and +# 2) the specularity for each feature. +# A specularity value of 100 is fully specular, useful for water +# A specularity value of 10 is more useful for inland features. +# Sand may also reflect some of its surface, so possibly 25 is good there. +mstr_xp_normal_maps = [ + ("landuse", "farmland", 10), + ("landuse", "forest", 10), + ("leisure", "nature_reserve", 10), + ("natural", "water", 100), + ("water", "pond", 100), + ("water", "river", 100), + ("water", "lake", 100) +] + +# How much of a tile we need for each zoom level. The higher +# the zoom level, the smaller the area to generate a mask of - but also +# higher detail. +mstr_zl_16 = 0.08 +mstr_zl_17 = 0.048 +mstr_zl_18 = 0.016 +mstr_zl_19 = 0.008 + + +# The layers we will process, from bottom to top. +# Tag, and value. +# Strong recommendation NOT to alter this list - +# generating the orthos depends on the pool of source +# material I provide for these layers. If you add your own +# OSM tag, and there is no source material, the script will +# most likely crash. +mstr_ortho_layers = [ + # Z-Order 0 + ("landuse", "residential", "landuse", "residential-boundary"), + ("boundary", "administrative", "admin_level", ["8", "9", "10", "12"], "landuse", "residential-boundary"), + # Z-Order 1 + ("landuse", "grass", "landuse", "grass"), + ("landuse", "cemetery", "landuse", "grass"), + ("landuse", "greenfield", "landuse", "grass"), + ("landuse", "orchard", "landuse", "meadow"), + ("landuse", "meadow", "landuse", "meadow"), + ("landuse", "recreation_ground", "landuse", "meadow"), + ("leisure", "golf_course", "leisure", "golf_course"), + ("barrier", "hedge", "natural", "heath"), + ("landuse", "vineyard", "landuse", "meadow"), + ("highway", "track", 3), + ("highway", "path", 3), + ("natural", "bare_rock", "natural", "bare_rock"), + ("natural", "grassland", "landuse", "meadow"), + ("leisure", "park", "leisure", "green"), + ("leisure", "dog_park", "leisure", "green"), + ("leisure", "garden", "leisure", "green"), + ("leisure", "sports_centre", "leisure", "green"), + ("leisure", "pitch", "leisure", "green"), + ("landuse", "farmland", "landuse", "farmland"), + ("landuse", "farmyard", "landuse", "farmland"), + ("landuse", "military", "landuse", "residential-boundary"), + # Z-Order 2 + ("highway", "service", 6), + ("highway", "residential", 12), + ("highway", "footway", 4), + ("highway", "primary", 25), + ("highway", "secondary", 13), + ("highway", "tertiary", 20), + ("highway", "unclassified", 17), + ("highway", "living_street", 12), + ("waterway", "river", 10), + ("waterway", "stream", 10), + ("leisure", "nature_reserve", "landuse", "forest"), + ("landuse", "forest", "landuse", "forest"), + ("natural", "wetland", "natural", "wetland"), + ("natural", "scrub", "natural", "scrub"), + ("natural", "heath", "natural", "heath"), + ("natural", "sand", "natural", "sand"), + ("natural", "desert", "natural", "desert"), + # Z-Order 3 + ("natural", "water", "natural", "water"), + ("natural", "bay", "natural", "beach"), + ("natural", "beach", "natural", "beach"), + ("water", "lake", "natural", "water"), + ("water", "pond", "natural", "water"), + ("water", "river", "natural", "water"), + ("amenity", "parking", "amenities", "parking"), + ("highway", "pedestrian", 4), + # Z-Order 4 + ("highway", "motorway", 32), + ("railway", "rail", 5), + # Z-Order 5 + ("aeroway", "taxiway", 42), + ("aeroway", "runway", 80), + ("building", "detached", "building", "common"), + ("building", "church", "building", "common"), + ("building", "hotel", "building", "industrial"), + ("building", "farm", "building", "industrial"), + ("building", "semidetached_house", "building", "common"), + ("building", "apartments", "building", "common"), + ("building", "civic", "building", "common"), + ("building", "garage", "building", "industrial"), + ("building", "office", "building", "office"), + ("building", "retail", "building", "industrial"), + ("building", "industrial", "building", "industrial"), + ("building", "house", "building", "house"), + ("building", "terrace", "building", "industrial"), + ("building", "hangar", "building", "industrial"), + ("building", "school", "building", "common"), + ("building", "yes", "building", "common"), + ("place", "sea", "natural", "sea"), + ("place", "ocean", "natural", "sea") +] + + +# Blur values for the single masks of the ortho layers +mstr_mask_blur = [ + # Z-Order 0 + ("landuse", "residential", 30), + ("boundary", "administrative", 30), + # Z-Order 1 + ("landuse", "grass", 30), + ("landuse", "cemetery", 30), + ("landuse", "greenfield", 30), + ("landuse", "orchard", 30), + ("landuse", "meadow", 30), + ("barrier", "hedge", 5), + ("landuse", "recreation_ground", 20), + ("landuse", "vineyard", 30), + ("natural", "grassland", 30), + ("natural", "wetland", 30), + ("natural", "scrub", 20), + ("natural", "heath", 20), + ("leisure", "park", 30), + ("leisure", "golf_course", 25), + ("leisure", "dog_park", 35), + ("leisure", "garden", 20), + ("leisure", "sports_centre", 5), + ("leisure", "pitch", 2), + ("landuse", "farmland", 10), + ("landuse", "farmyard", 10), + # Z-Order 2 + ("landuse", "forest", 20), + ("leisure", "nature_reserve", 20), + ("landuse", "military", 30), + # Z-Order 3 + ("natural", "bare_rock", 25), + ("natural", "water", 4), + ("natural", "bay", 30), + ("natural", "beach", 30), + ("water", "lake", 10), + ("water", "pond", 10), + ("water", "river", 10), + ("waterway", "river", 10), + ("waterway", "stream", 10), + ("amenity", "parking", 3), + ("highway", "pedestrian", 12), + # Z-Order 4 + ("highway", "motorway", 5), + ("highway", "primary", 5), + ("highway", "secondary", 5), + ("highway", "tertiary", 5), + ("highway", "unclassified", 5), + ("highway", "living_street", 5), + ("highway", "residential", 5), + ("highway", "service", 3), + ("highway", "footway", 3), + ("highway", "track", 3), + ("highway", "path", 3), + ("railway", "rail", 4), + # Z-Order 5 + ("aeroway", "taxiway", 12), + ("aeroway", "runway", 12), + ("building", "detached", 1), + ("building", "church", 1), + ("building", "hotel", 1), + ("building", "farm", 1), + ("building", "semidetached_house", 1), + ("building", "apartments", 1), + ("building", "civic", 1), + ("building", "garage", 1), + ("building", "office", 1), + ("building", "retail", 1), + ("building", "industrial", 1), + ("building", "house", 1), + ("building", "terrace", 1), + ("building", "hangar", 1), + ("building", "school", 1), + ("building", "yes", 1), + ("place", "sea", 1), + ("place", "ocean", 1) +] diff --git a/doc/Thumbs.db b/doc/Thumbs.db new file mode 100644 index 0000000..aed0fc9 Binary files /dev/null and b/doc/Thumbs.db differ diff --git a/doc/og_01.jpg b/doc/og_01.jpg new file mode 100644 index 0000000..ca0b1bb Binary files /dev/null and b/doc/og_01.jpg differ diff --git a/doc/phase 2/1_1.jpg b/doc/phase 2/1_1.jpg new file mode 100644 index 0000000..430fa74 Binary files /dev/null and b/doc/phase 2/1_1.jpg differ diff --git a/doc/phase 2/1_10.jpg b/doc/phase 2/1_10.jpg new file mode 100644 index 0000000..53b8e3b Binary files /dev/null and b/doc/phase 2/1_10.jpg differ diff --git a/doc/phase 2/1_11.jpg b/doc/phase 2/1_11.jpg new file mode 100644 index 0000000..e903c99 Binary files /dev/null and b/doc/phase 2/1_11.jpg differ diff --git a/doc/phase 2/1_12.jpg b/doc/phase 2/1_12.jpg new file mode 100644 index 0000000..8dff35c Binary files /dev/null and b/doc/phase 2/1_12.jpg differ diff --git a/doc/phase 2/1_13.jpg b/doc/phase 2/1_13.jpg new file mode 100644 index 0000000..819cafb Binary files /dev/null and b/doc/phase 2/1_13.jpg differ diff --git a/doc/phase 2/1_14.jpg b/doc/phase 2/1_14.jpg new file mode 100644 index 0000000..dd86b97 Binary files /dev/null and b/doc/phase 2/1_14.jpg differ diff --git a/doc/phase 2/1_15.jpg b/doc/phase 2/1_15.jpg new file mode 100644 index 0000000..7839db5 Binary files /dev/null and b/doc/phase 2/1_15.jpg differ diff --git a/doc/phase 2/1_16.jpg b/doc/phase 2/1_16.jpg new file mode 100644 index 0000000..96ccf08 Binary files /dev/null and b/doc/phase 2/1_16.jpg differ diff --git a/doc/phase 2/1_17.jpg b/doc/phase 2/1_17.jpg new file mode 100644 index 0000000..6b13346 Binary files /dev/null and b/doc/phase 2/1_17.jpg differ diff --git a/doc/phase 2/1_18.jpg b/doc/phase 2/1_18.jpg new file mode 100644 index 0000000..fe5a479 Binary files /dev/null and b/doc/phase 2/1_18.jpg differ diff --git a/doc/phase 2/1_19.jpg b/doc/phase 2/1_19.jpg new file mode 100644 index 0000000..c555fdb Binary files /dev/null and b/doc/phase 2/1_19.jpg differ diff --git a/doc/phase 2/1_2.jpg b/doc/phase 2/1_2.jpg new file mode 100644 index 0000000..521638d Binary files /dev/null and b/doc/phase 2/1_2.jpg differ diff --git a/doc/phase 2/1_20.jpg b/doc/phase 2/1_20.jpg new file mode 100644 index 0000000..ebcda54 Binary files /dev/null and b/doc/phase 2/1_20.jpg differ diff --git a/doc/phase 2/1_21.jpg b/doc/phase 2/1_21.jpg new file mode 100644 index 0000000..7ed6c23 Binary files /dev/null and b/doc/phase 2/1_21.jpg differ diff --git a/doc/phase 2/1_22.jpg b/doc/phase 2/1_22.jpg new file mode 100644 index 0000000..6d1a9ce Binary files /dev/null and b/doc/phase 2/1_22.jpg differ diff --git a/doc/phase 2/1_23.jpg b/doc/phase 2/1_23.jpg new file mode 100644 index 0000000..29cd1b2 Binary files /dev/null and b/doc/phase 2/1_23.jpg differ diff --git a/doc/phase 2/1_24.jpg b/doc/phase 2/1_24.jpg new file mode 100644 index 0000000..42b5705 Binary files /dev/null and b/doc/phase 2/1_24.jpg differ diff --git a/doc/phase 2/1_25.jpg b/doc/phase 2/1_25.jpg new file mode 100644 index 0000000..27b2e49 Binary files /dev/null and b/doc/phase 2/1_25.jpg differ diff --git a/doc/phase 2/1_3.jpg b/doc/phase 2/1_3.jpg new file mode 100644 index 0000000..6096f0a Binary files /dev/null and b/doc/phase 2/1_3.jpg differ diff --git a/doc/phase 2/1_4.jpg b/doc/phase 2/1_4.jpg new file mode 100644 index 0000000..2c8ce0c Binary files /dev/null and b/doc/phase 2/1_4.jpg differ diff --git a/doc/phase 2/1_5.jpg b/doc/phase 2/1_5.jpg new file mode 100644 index 0000000..d5b20f8 Binary files /dev/null and b/doc/phase 2/1_5.jpg differ diff --git a/doc/phase 2/1_6.jpg b/doc/phase 2/1_6.jpg new file mode 100644 index 0000000..5fab5a1 Binary files /dev/null and b/doc/phase 2/1_6.jpg differ diff --git a/doc/phase 2/1_7.jpg b/doc/phase 2/1_7.jpg new file mode 100644 index 0000000..e2cb039 Binary files /dev/null and b/doc/phase 2/1_7.jpg differ diff --git a/doc/phase 2/1_8.jpg b/doc/phase 2/1_8.jpg new file mode 100644 index 0000000..9d1710d Binary files /dev/null and b/doc/phase 2/1_8.jpg differ diff --git a/doc/phase 2/1_9.jpg b/doc/phase 2/1_9.jpg new file mode 100644 index 0000000..f0029ac Binary files /dev/null and b/doc/phase 2/1_9.jpg differ diff --git a/doc/rw_01.jpg b/doc/rw_01.jpg new file mode 100644 index 0000000..7a2e878 Binary files /dev/null and b/doc/rw_01.jpg differ diff --git a/doc/test10.jpg b/doc/test10.jpg new file mode 100644 index 0000000..42b5705 Binary files /dev/null and b/doc/test10.jpg differ diff --git a/doc/test11.jpg b/doc/test11.jpg new file mode 100644 index 0000000..10f6ed3 Binary files /dev/null and b/doc/test11.jpg differ diff --git a/doc/test7.jpg b/doc/test7.jpg new file mode 100644 index 0000000..430fa74 Binary files /dev/null and b/doc/test7.jpg differ diff --git a/doc/test8.jpg b/doc/test8.jpg new file mode 100644 index 0000000..9d1710d Binary files /dev/null and b/doc/test8.jpg differ diff --git a/doc/test9.jpg b/doc/test9.jpg new file mode 100644 index 0000000..e903c99 Binary files /dev/null and b/doc/test9.jpg differ diff --git a/functions.py b/functions.py index 1cd410b..9f02251 100644 --- a/functions.py +++ b/functions.py @@ -1,86 +1,86 @@ - -# ------------------------------------------------------------------- -# ORTHOGRAPHIC -# Your personal aerial satellite. Always on. At any altitude.* -# Developed by MarStrMind -# License: Open Software License 3.0 -# Up to date version always on marstr.online -# ------------------------------------------------------------------- -# functions.py -# A number of utility functions that don't belong in any class -# ------------------------------------------------------------------- - -import os -import json -from defines import * - - -# Find out if we have a tiledata.json file at all -def doesTileDataExist(lat, lng): - e = False - if os.path.isfile(mstr_datafolder + "Tiles/" + str(lat) + "_" + str(lng) + "/data.db"): - e = True - return e - - - -# Find adjacent tiles to check against existing chosen imagery -# -# The provided tile is the center, and it will provide a list of -# all adjacent tiles, like so: -# -# # -# #X# -# # -# -# If we are in a corner, those will be omitted. -def findAdjacentTilesTo(v, h): - adj = [] - adj.append( (v+1, h) ) # Top - adj.append( (v, h+1) ) # Right - adj.append( (v-1, h) ) # Bottom - adj.append( (v, h-1) ) # Left - - return adj - - -# Find the tiles we need in order to construct zoom level 16 photos, -# based on the tile provided. This is usually called at the very end -# of the ortho generation process. -def findZL16tiles(v, h): - # Contains the tiles, in forward order (order of processing) - tiles = [] - - for y in range(0, 4): - tr = [] - for x in range(0, 4): - tl = (v+y, h+x) - tr.append(tl) - tiles.append(tr) - - return tiles - - -# Find the tiles to keep around an airport, using the defined tile -# radius amount in defines.py -def findAirportTiles(av, ah): - # The tiles - tiles=[] - - # Starting points - sty = av - int(mstr_airport_radius/2) - stx = ah - int(mstr_airport_radius/2) - - for y in range(mstr_airport_radius): - for x in range(mstr_airport_radius): - a = ( sty+y, stx+x ) - tiles.append(a) - - # Return the tiles - return tiles - - -# Testing -def in_circle(center_x, center_y, radius, x, y): - square_dist = (center_x - x) ** 2 + (center_y - y) ** 2 + +# ------------------------------------------------------------------- +# ORTHOGRAPHIC +# Your personal aerial satellite. Always on. At any altitude.* +# Developed by MarStrMind +# License: Open Software License 3.0 +# Up to date version always on marstr.online +# ------------------------------------------------------------------- +# functions.py +# A number of utility functions that don't belong in any class +# ------------------------------------------------------------------- + +import os +import json +from defines import * + + +# Find out if we have a tiledata.json file at all +def doesTileDataExist(lat, lng): + e = False + if os.path.isfile(mstr_datafolder + "Tiles/" + str(lat) + "_" + str(lng) + "/data.db"): + e = True + return e + + + +# Find adjacent tiles to check against existing chosen imagery +# +# The provided tile is the center, and it will provide a list of +# all adjacent tiles, like so: +# +# # +# #X# +# # +# +# If we are in a corner, those will be omitted. +def findAdjacentTilesTo(v, h): + adj = [] + adj.append( (v+1, h) ) # Top + adj.append( (v, h+1) ) # Right + adj.append( (v-1, h) ) # Bottom + adj.append( (v, h-1) ) # Left + + return adj + + +# Find the tiles we need in order to construct zoom level 16 photos, +# based on the tile provided. This is usually called at the very end +# of the ortho generation process. +def findZL16tiles(v, h): + # Contains the tiles, in forward order (order of processing) + tiles = [] + + for y in range(0, 4): + tr = [] + for x in range(0, 4): + tl = (v+y, h+x) + tr.append(tl) + tiles.append(tr) + + return tiles + + +# Find the tiles to keep around an airport, using the defined tile +# radius amount in defines.py +def findAirportTiles(av, ah): + # The tiles + tiles=[] + + # Starting points + sty = av - int(mstr_airport_radius/2) + stx = ah - int(mstr_airport_radius/2) + + for y in range(mstr_airport_radius): + for x in range(mstr_airport_radius): + a = ( sty+y, stx+x ) + tiles.append(a) + + # Return the tiles + return tiles + + +# Testing +def in_circle(center_x, center_y, radius, x, y): + square_dist = (center_x - x) ** 2 + (center_y - y) ** 2 return square_dist <= radius ** 2 \ No newline at end of file diff --git a/layergen.py b/layergen.py index 2dde29c..9b47552 100644 --- a/layergen.py +++ b/layergen.py @@ -1,781 +1,781 @@ - -# ------------------------------------------------------------------- -# ORTHOGRAPHIC -# Your personal aerial satellite. Always on. At any altitude.* -# Developed by MarStrMind -# License: Open Software License 3.0 -# Up to date version always on marstr.online -# ------------------------------------------------------------------- -# layergen.py -# Generates a full-sized geo layer image, based on the required layer -# type. We use a simple randomization method to generate such an -# image, which is then used for the final photo in photogen. -# ------------------------------------------------------------------- - -import glob -import os -from random import randrange -import random -from PIL import Image, ImageFilter, ImageDraw, ImagePath -from defines import * -from log import * -from tiledb import * -from osmxml import * -from functions import * -from xp_normalmap import * - -class mstr_layergen: - - # Initializes the layer generator. can_choose will go false if we need - # a pre-determined layer from another tile, should this be adjacent to it. - # In this case layer_needed will be populated with the appropriate number. - # You also need the zoom level so that we can generate a scaled version. - def __init__(self, tag, value, lat, latnum, lng, lngnum, is_line, is_completion=False): - self._tag = tag - self._value = value - self._latitude = lat - self._lat_number = latnum - self._longitude = lng - self._lng_number = lngnum - self._layerborder = -1 - self._is_completion = is_completion - # Define layer size depending on what is wanted - self._imgsize = 0 - self._isline = is_line - if mstr_photores == 2048: self._imgsize = 3000 - if mstr_photores == 4096: self._imgsize = 6000 - #mstr_msg("layergen", "Layer gen initialized") - - # Define maximum latitude and longitude tile numbers - def set_max_latlng_tile(self, maxlatlng): - self._maxlat = maxlatlng[0] - self._maxlng = maxlatlng[1] - mstr_msg("layergen", "Maximum latitude and longitude tile numbers received") - - # Set latlng folder - def set_latlng_folder(self, latlngfld): - self._latlngfld = latlngfld - - # Open DB - def open_db(self): - self._tiledb = mstr_tiledb(self._latitude, self._longitude, self._latlngfld) - self._tiledb.create_tables() - - # This generates a "border" image, for example farmland usually has a small space of grass - # before the actual crop of farm field itself. This generates this "border" layer, - # and returns it. - # Needs the actual edge mask, and the tag and value to be used as border. - # Perform necessary adjustments on the mask prior to this call, for example blurring or - # other effects. - def genborder(self, edgemask, tag, value): - layer = Image.new("RGBA", (self._imgsize, self._imgsize)) - root_folder = mstr_datafolder + "textures/" + tag + "/" + value - - # Determine which sources we use - brd = glob.glob(root_folder + "/brd/b*.png") - src = -1 - if len(brd) == 1: src=1 - if len(brd) >= 2: - src = randrange(1, len(brd)+1) - ptc = glob.glob(root_folder + "/ptc/b" + str(src) + "_p*.png") - - # Load in the sources to work with - brd_src = Image.open(root_folder + "/brd/b" + str(src) + ".png") - ptc_src = [] - for p in ptc: - ptc_src.append(Image.open(p)) - mstr_msg("layergen", "Border sources selected") - - # Begin producing a largely random image - samples = 250 # <- We need this in a moment - for i in range(samples): - imgid = 0 - if len(ptc_src) == 1: imgid = 0 - if len(ptc_src) >= 2: - imgid = randrange(1, len(ptc_src)+1) - 1 - l = 0 - int(ptc_src[imgid].width / 2) - r = layer.width - int(ptc_src[imgid].width / 2) - t = 0 - int(ptc_src[imgid].height / 2) - b = layer.height - int(ptc_src[imgid].height / 2) - layer.alpha_composite( ptc_src[imgid], ( randrange(l, r), randrange(t, b) ) ) - mstr_msg("layergen", "Border image generated") - - # We now need to add the seamless border - layer.alpha_composite( brd_src ) - mstr_msg("layergen", "Layer image completed") - - # And now for the Big Mac. - # Generate the layer from the mask. - layer_comp = Image.new("RGBA", (self._imgsize, self._imgsize)) - layer_final = Image.composite(layer, layer_comp, edgemask) - - # Provide the image - return layer_final - - - # This generates the layer from the defined mask - def genlayer(self): - - mstr_msg("layergen", "Layer to be generated: " + str(self._latitude) + "-" + str(self._lat_number) + ":" + str(self._longitude) + "-" + str(self._lng_number) + " -- tag: " + self._tag + " - value: " + self._value ) - - # Before we generate the layer, let's check for airports in this chunk - mstr_msg("layergen", "Checking for airport/s with ICAO code") - osmxml = mstr_osmxml(0,0) - icao = osmxml.find_icao_codes(mstr_datafolder + "_cache/tile.xml") - mstr_msg("layergen", "Found " + str(len(icao)) + " airport/s") - # Runway surface, if any other than concrete/asphalt - rw_surface = "" - # If we find an airport, make a note ... - if len(icao) >= 1: - for i in icao: - # ... but only, if this airport is not already noted - iccheck = self._tiledb.perform_query("SELECT * FROM airports WHERE icao='" + i +"';") - if len(iccheck) == 0: - self._tiledb.insert_icao(i, self._lat_number, self._lng_number, self._latitude, self._longitude) - mstr_msg("layergen", "Airport/s noted in data file") - rw_surface = osmxml.find_runway_surface(mstr_datafolder + "_cache/tile.xml") - - # The image for the layer itself - layer = Image.new("RGBA", (self._imgsize, self._imgsize)) - layer_pix = layer.load() - - # There are some things we need to use sources for, and some things, we do not. - # We need to differentiate that. - - if (self._isline == False and self._tag != "building") or (self._is_completion == True): - # Determine where we get the our source material from - root_folder = mstr_datafolder + "textures/" - for s in mstr_ortho_layers: - if s[0] == self._tag and s[1] == self._value: - fld_main = len(s)-2 - fld_sub = len(s)-1 - root_folder = root_folder + s[fld_main] + "/" + s[fld_sub] - - # Determine which sources to use. - # First, we need to check for adjacent tile information. We then either - # need to use the source of any adjacent tile, or we can choose freely. - src = -1 - - # Find our adjacent tiles - adjtiles = findAdjacentTilesTo(self._lat_number, self._lng_number) - - mstr_msg("layergen", "Performing adjacency check") - # Walk through each tile and see what we can find in relation to this - # tile in the center - # Since we already know the order in adjtiles, we can do this real easy - if self._is_completion == False: - at = self._tiledb.get_adjacency_for_source(adjtiles[0][0], adjtiles[0][1], self._tag, self._value) # Top - ar = self._tiledb.get_adjacency_for_source(adjtiles[1][0], adjtiles[1][1], self._tag, self._value) # Right - ab = self._tiledb.get_adjacency_for_source(adjtiles[2][0], adjtiles[2][1], self._tag, self._value) # Bottom - al = self._tiledb.get_adjacency_for_source(adjtiles[3][0], adjtiles[3][1], self._tag, self._value) # Left - if self._is_completion == True: - at = self._tiledb.get_adjacency_for_completion(adjtiles[0][0], adjtiles[0][1]) # Top - ar = self._tiledb.get_adjacency_for_completion(adjtiles[1][0], adjtiles[1][1]) # Right - ab = self._tiledb.get_adjacency_for_completion(adjtiles[2][0], adjtiles[2][1]) # Bottom - al = self._tiledb.get_adjacency_for_completion(adjtiles[3][0], adjtiles[3][1]) # Left - - if len(at) == 1: - self._tag = at[0][2] - self._value = at[0][3] - if len(ar) == 1: - self._tag = ar[0][2] - self._value = ar[0][3] - if len(ab) == 1: - self._tag = ab[0][2] - self._value = ab[0][3] - if len(al) == 1: - self._tag = al[0][2] - self._value = al[0][3] - - root_folder = mstr_datafolder + "textures/" - for s in mstr_ortho_layers: - if s[0] == self._tag and s[1] == self._value: - fld_main = len(s)-2 - fld_sub = len(s)-1 - root_folder = root_folder + s[fld_main] + "/" + s[fld_sub] - - # We are south to the top tile. - if len(at) == 1 and src == -1: - if "b" in at[0][5]: src = int(at[0][4]) - # We are west to the right tile. - if len(ar) == 1 and src == -1: - if "l" in ar[0][5]: src = int(ar[0][4]) - # We are north to the bottom tile. - if len(ab) == 1 and src == -1: - if "t" in ab[0][5]: src = int(ab[0][4]) - # We are east to the left tile. - if len(al) == 1 and src == -1: - if "r" in al[0][5]: src = int(al[0][4]) - - # Should we be at the border of the degree for latitude and longitude, we need to perform - # additional checks - is_deg_brd_t = False - is_deg_brd_r = False - is_deg_brd_b = False - is_deg_brd_l = False - if self._lat_number == 1: is_deg_brd_b = True - if self._lat_number == self._maxlat: is_deg_brd_t = True - if self._lng_number == 1: is_deg_brd_l = True - if self._lng_number == self._maxlng: is_deg_brd_r = True - - # Adjacent latitude and longitude tiles - deg_tiles = [] - deg_tiles.append( ( self._latitude+1, self._longitude ) ) # Top - deg_tiles.append( ( self._latitude, self._longitude+1 ) ) # Right - deg_tiles.append( ( self._latitude-1, self._longitude ) ) # Bottom - deg_tiles.append( ( self._latitude, self._longitude-1 ) ) # Left - - # Perform degree border checks - # - and make sure we do not run into errors - this drove me crazy in testing - atd = [] - ard = [] - abd = [] - ald = [] - if is_deg_brd_t == True: - if self._is_completion == False: - atd = self._tiledb.get_adjacency_for_source_in_lat_lng(deg_tiles[0][0], deg_tiles[0][1], 1, self._lng_number, self._tag, self._value) # Top - if self._is_completion == True: - atd = self._tiledb.get_adjacency_for_completion_in_lat_lng(deg_tiles[0][0], deg_tiles[0][1], 1, self._lng_number) # Top - if is_deg_brd_r == True: - if self._is_completion == False: - ard = self._tiledb.get_adjacency_for_source_in_lat_lng(deg_tiles[1][0], deg_tiles[1][1], self._lat_number, 1, self._tag, self._value) # Right - if self._is_completion == True: - ard = self._tiledb.get_adjacency_for_completion_in_lat_lng(deg_tiles[1][0], deg_tiles[1][1], self._lat_number, 1) # Right - if is_deg_brd_b == True: - maxlatlng = self._tiledb.get_highest_latlong_from_tile(self._latitude-1, self._longitude) - if self._is_completion == False: - abd = self._tiledb.get_adjacency_for_source_in_lat_lng(deg_tiles[2][0], deg_tiles[2][1], maxlatlng[0], self._lng_number, self._tag, self._value) # Bottom - if self._is_completion == True: - abd = self._tiledb.get_adjacency_for_completion_in_lat_lng(deg_tiles[2][0], deg_tiles[2][1], maxlatlng[0], self._lng_number) # Bottom - if is_deg_brd_l == True: - maxlatlng = self._tiledb.get_highest_latlong_from_tile(self._latitude, self._longitude-1) - if self._is_completion == False: - ald = self._tiledb.get_adjacency_for_source_in_lat_lng(deg_tiles[2][0], deg_tiles[2][1], self._lat_number, maxlatlng[1], self._tag, self._value) # Left - if self._is_completion == True: - ald = self._tiledb.get_adjacency_for_completion_in_lat_lng(deg_tiles[2][0], deg_tiles[2][1], self._lat_number, maxlatlng[1]) # Left - - if (is_deg_brd_t == True or is_deg_brd_r == True or is_deg_brd_b == True or is_deg_brd_l == True): - if src == -1 and self._is_completion == True: - if len(atd) == 1: - self._tag = atd[0][2] - self._value = atd[0][3] - if len(ard) == 1: - self._tag = ard[0][2] - self._value = ard[0][3] - if len(abd) == 1: - self._tag = abd[0][2] - self._value = abd[0][3] - if len(ald) == 1: - self._tag = ald[0][2] - self._value = ald[0][3] - - root_folder = mstr_datafolder + "textures/" - for s in mstr_ortho_layers: - if s[0] == self._tag and s[1] == self._value: - fld_main = len(s)-2 - fld_sub = len(s)-1 - root_folder = root_folder + s[fld_main] + "/" + s[fld_sub] - - - # Should we get here and one of the degree border checks turns out true, - # we need to make sure that we select the source we found. This should - # enable seamless tiling... around the entire planet - if is_deg_brd_t == True and len(at) == 0 and src == -1: - if len(atd) == 1: - if "b" in atd[0][5]: src = int(atd[0][4]) - if is_deg_brd_r == True and len(ar) == 0 and src == -1: - if len(ard) == 1: - if "l" in ard[0][5]: src = int(ard[0][4]) - if is_deg_brd_b == True and len(ab) == 0 and src == -1: - if len(abd) == 1: - if "t" in abd[0][5]: src = int(abd[0][4]) - if is_deg_brd_l == True and len(al) == 0 and src == -1: - if len(ald) == 1: - if "r" in ald[0][5]: src = int(ald[0][4]) - - mstr_msg("layergen", "Adjacency check completed") - - brd = glob.glob(root_folder + "/brd/b*.png") - - # If the adjacency check returned nothing (src is still -1), - # then pick something - if src == -1: - if len(brd) == 1: src=1 - if len(brd) >= 2: - src = randrange(1, len(brd)+1) - - ptc = glob.glob(root_folder + "/ptc/b" + str(src) + "_p*.png") - - # Load in the sources to work with - brd_src = Image.open(root_folder + "/brd/b" + str(src) + ".png") - ptc_src = [] - for p in ptc: - ptc_src.append(Image.open(p)) - mstr_msg("layergen", "Layer sources selected") - - # OK! Load the mask - if self._is_completion == False: - osm_mask = Image.open( mstr_datafolder + "_cache/" + str(self._latitude) + "-" + str(self._lat_number) + "_" + str(self._longitude) + "-" + str(self._lng_number) + "_" + self._tag + "-" + self._value + ".png" ) - if self._is_completion == True: - osm_mask = Image.open( mstr_datafolder + "_cache/" + str(self._latitude) + "-" + str(self._lat_number) + "_" + str(self._longitude) + "-" + str(self._lng_number) + "_tile-completion.png" ) - - # Generate an edge mask from the original - osm_edge = osm_mask.filter(ImageFilter.FIND_EDGES) - osm_edge = osm_edge.filter(ImageFilter.MaxFilter) - mstr_msg("layergen", "Edge mask generated") - - # This adds some natural looking shapes to these types of features - if self._value == "forest" or self._value == "nature_reserve": - epx = osm_edge.load() - imgd = ImageDraw.Draw(osm_mask) - - # Walk through a grid of 200x200 - on the edge image - for y in range(0, osm_mask.height, int(osm_mask.height/200)): - for x in range(0, osm_mask.width, int(osm_mask.width/200)): - px = epx[x,y] - if px[3] == 255: - rx = randrange(24,60) - ry = randrange(24,60) - f = randrange(1,10) - - # Randomize the found locations a little - psx = randrange(x-11, x+11) - psy = randrange(y-11, y+11) - - # Do some magic - but not on edges - if x > 0 and x < osm_mask.width and y > 0 and y < osm_mask.height: - if f != 5: - imgd.ellipse((psx-int(rx/2), psy-int(ry/2), psx+rx, psy+ry), fill="black") - if f == 3 or f == 7: - imgd.ellipse((psx-int(rx/2), psy-int(ry/2), psx+rx, psy+ry), fill=(0,0,0,0)) - - - # We need to change the image in certain conditions - if self._value == "hedge" and self._tag == "barrier": - osm_mask = osm_edge - - # From here on in we will need to perform some adjustments on the masks, depending - # on what they are. - for i in mstr_mask_blur: - if i[0] == self._tag and i[1] == self._value: - if self._tag != "place" and (self._value != "sea" or self._value != "ocean"): - osm_mask = osm_mask.filter(ImageFilter.BoxBlur(radius=i[2])) - break - - # Begin producing a largely random image - samples = 250 # <- We need this in a moment - for i in range(samples): - imgid = 0 - if len(ptc_src) == 1: imgid = 0 - if len(ptc_src) >= 2: - imgid = randrange(1, len(ptc_src)+1) - 1 - l = 0 - int(ptc_src[imgid].width / 2) - r = layer.width - int(ptc_src[imgid].width / 2) - t = 0 - int(ptc_src[imgid].height / 2) - b = layer.height - int(ptc_src[imgid].height / 2) - layer.alpha_composite( ptc_src[imgid], ( randrange(l, r), randrange(t, b) ) ) - mstr_msg("layergen", "Layer image generated") - - - # Here we need to do some magic to make some features look more natural - if (self._tag == "landuse" and self._value == "meadow") or (self._tag == "natural" and self._value == "grassland") or (self._tag == "natural" and self._value == "heath"): - amt = randrange(1,5) - for i in range(1, amt+1): - ptc = randrange(1, 14) - img = Image.open(mstr_datafolder + "textures/tile/completion/p" + str(ptc)+".png") - lx = randrange( int(layer.width/20), layer.width - (int(layer.width/20)) - img.width ) - ly = randrange( int(layer.width/20), layer.width - (int(layer.width/20)) - img.height ) - layer.alpha_composite( img, (lx, ly) ) - - - # We now need to add the seamless border - layer.alpha_composite( brd_src ) - mstr_msg("layergen", "Layer image completed") - - - # And now for the Big Mac. - # Generate the layer from the mask. - layer_comp = Image.new("RGBA", (self._imgsize, self._imgsize)) - layer_pix = layer.load() - mask_pix = osm_mask.load() - layer_comp_pix = layer_comp.load() - for y in range(self._imgsize): - for x in range(self._imgsize): - if mask_pix[x, y][3] > 0: - rgb=layer_pix[x,y] - a=mask_pix[x,y] - if self._value == "residential": - layer_comp_pix[x, y] = ( rgb[0], rgb[1], rgb[2], int(a[3]/2)) - if self._value != "residential": - layer_comp_pix[x, y] = ( rgb[0], rgb[1], rgb[2], a[3]) - - # For some things, we will need to add a border and then add this to the layer. - layer_border = None - if self._tag == "landuse": - if self._value == "forest" or self._value == "farmland": - osm_edge = osm_edge.filter(ImageFilter.ModeFilter(size=15)) - osm_edge = osm_edge.filter(ImageFilter.BoxBlur(radius=2)) - layer_border = self.genborder(osm_edge, "landuse", "meadow") - layer_comp.alpha_composite(layer_border) - - # Store layer - if self._is_completion == False: - layer_comp.save( mstr_datafolder + "_cache/" + str(self._latitude) + "-" + str(self._lat_number) + "_" + str(self._longitude) + "-" + str(self._lng_number) + "_" + self._tag + "-" + self._value + "_layer.png" ) - if self._is_completion == True: - layer_comp.save( mstr_datafolder + "_cache/" + str(self._latitude) + "-" + str(self._lat_number) + "_" + str(self._longitude) + "-" + str(self._lng_number) + "_tile-completion_layer.png" ) - #layer_final.save( mstr_datafolder + "_cache/" + str(self._latitude) + "-" + str(self._lat_number) + "_" + str(self._longitude) + "-" + str(self._lng_number) + "_" + self._tag + "-" + self._value + "_layer.png" ) - mstr_msg("layergen", "Layer image finalized and saved.") - - - # Depending on if scenery for XP should be made, AND if normal maps should be made, we would - # need to make them at this exact point - if mstr_xp_genscenery == True: - if mstr_xp_generate_normal_maps == True: - nm = False - for n in mstr_xp_normal_maps: - if n[0] == self._tag and n[1] == self._value: - nm = True - break - if nm == True: - nrm = mstr_xp_normalmap(self._latitude, self._longitude, self._tag, self._value, self._lat_number, self._lng_number, self._latlngfld) - nrm.build_normalmap() - - - # Let's try our hand at pseudo shadows - if mstr_shadow_enabled == True: - if mstr_shadow_shift >= 2: - shadow = Image.new("RGBA", (self._imgsize, self._imgsize)) - for sh in mstr_shadow_casters: - if self._tag == sh[0] and self._value == sh[1]: - mstr_msg("layergen", "Generating shadow for layer") - shadow_pix = shadow.load() - mask_pix = osm_mask.load() - for y in range(self._imgsize-1): - for x in range(self._imgsize-1): - m = mask_pix[x,y] - shf_x = 0 - # Buildings get slightly closer shadows - if self._tag == "building": - shf_x = x + int(mstr_shadow_shift/2) - if self._tag != "building": - shf_x = x + mstr_shadow_shift - if shf_x <= self._imgsize-1: - a = mask_pix[x,y][3] - st = 0 - if self._tag == "building": - st = random.uniform(0.25, mstr_shadow_strength/2) - if self._tag != "building": - st = random.uniform(0.45, mstr_shadow_strength) - ca = a * st - aa = int(ca) - shadow_pix[shf_x, y] = (0,0,0,aa) - shadow.save(mstr_datafolder + "_cache/" + str(self._latitude) + "-" + str(self._lat_number) + "_" + str(self._longitude) + "-" + str(self._lng_number) + "_" + self._tag + "-" + self._value + "_layer_shadow.png") - mstr_msg("layergen", "Shadow layer completed") - - - - # Check if pixels touch the borders of the image, and if so - - # make a not of that in the database. - at=False - ar=False - ab=False - al=False - layer_pix = layer_comp.load() # <- Just to be safe - - # Top scan - for i in range(0, self._imgsize-1): - p = layer_pix[i,0] - if p[3] > 0: - at=True - break - - # Right scan - for i in range(0, self._imgsize-1): - p = layer_pix[self._imgsize-1,i] - if p[3] > 0: - ar=True - break - - # Bottom scan - for i in range(0, self._imgsize-1): - p = layer_pix[i,self._imgsize-1] - if p[3] > 0: - ab=True - break - - # Left scan - for i in range(0, self._imgsize-1): - p = layer_pix[1,i] - if p[3] > 0: - al=True - break - - # Construct DB String - adjstr = "" - if at==True: adjstr = adjstr + "t" - if ar==True: adjstr = adjstr + "r" - if ab==True: adjstr = adjstr + "b" - if al==True: adjstr = adjstr + "l" - - # Store into DB - but only if there is something to store - if adjstr != "": - if self._is_completion == False: - r = self._tiledb.get_adjacency_for_source(self._lat_number, self._lng_number, self._tag, self._value) - if len(r) == 0: - self._tiledb.insert_info(self._lat_number, self._lng_number, self._tag, self._value, src, adjstr) - mstr_msg("layergen", "Adjacency info stored in database") - - if self._is_completion == True: - r = self._tiledb.get_adjacency_for_completion(self._lat_number, self._lng_number) - if len(r) == 0: - self._tiledb.insert_completion_info(self._lat_number, self._lng_number, self._tag, self._value, src, adjstr) - mstr_msg("layergen", "Adjacency info for completion stored in database") - - self._tiledb.commit_query() - self._tiledb.close_db() - - - # If we encounter one of these road-specific tags, we need to proceed differently. - - if self._isline == True or self._tag == "building": - - # We will need the mask in question - osm_mask = Image.open( mstr_datafolder + "_cache/" + str(self._latitude) + "-" + str(self._lat_number) + "_" + str(self._longitude) + "-" + str(self._lng_number) + "_" + self._tag + "-" + self._value + ".png" ) - - # Generate an edge mask from the original - osm_edge = osm_mask.filter(ImageFilter.FIND_EDGES) - osm_edge = osm_edge.filter(ImageFilter.MaxFilter) - mstr_msg("layergen", "Edge mask generated") - - # As above, we will apply the blur as noted in the defines - for i in mstr_mask_blur: - if i[0] == self._tag and i[1] == self._value: - osm_mask = osm_mask.filter(ImageFilter.BoxBlur(radius=i[2])) - break - osm_edge = osm_edge.filter(ImageFilter.BoxBlur(radius=1)) - - - # And now for the Big Mac. - # Generate the layer from the mask. Same as above - except! - # This time we have no source material - instead we will fill the - # mask with a color that is appropriate for this street type. - layer_comp = Image.new("RGBA", (self._imgsize, self._imgsize)) - mask_pix = osm_mask.load() - edge_pix = osm_edge.load() - layer_comp_pix = layer_comp.load() - - # Let's define some base color ranges for different types of buildings - bld_clr = [ - ("detached", 190, 192, 195), - ("church", 134, 134, 136), - ("hotel", 153, 147, 138), - ("farm", 145, 124, 121), - ("semidetached_house", 167, 163, 152), - ("apartments", 129, 134, 127), - ("civic", 134, 134, 136), - ("garage", 101, 109, 111), - ("office", 139, 152, 156), - ("retail", 121, 122, 108), - ("industrial", 191, 192, 187), - ("house", 145, 124, 121), - ("terrace", 191, 192, 187), - ("hangar", 137, 162, 195), - ("school", 111, 117, 115), - ("yes", 152, 144, 141) - ] - - # Find the color index to work with - cidx = 0 - if self._tag == "building": - for c in bld_clr: - if c[0] == self._value: - break - cidx = cidx+1 - - for y in range(self._imgsize): - for x in range(self._imgsize): - if mask_pix[x, y][3] > 0: - a=mask_pix[x,y] - e=edge_pix[x,y] - # Find a suitable color - d = 0 - if self._tag == "aeroway" and self._value == "runway": - # It seems only runways with any other surface than concrete - # are mentioned in OSM. So we need to make sure when to render - # "concrete" and when to leave it. Only sometimes the word - # "asphalt" is mentioned - if rw_surface == "" or rw_surface == "asphalt": - d = randrange(81, 101) - layer_comp_pix[x, y] = ( d,d,d,a[3] ) - if self._tag == "railway": - d = randrange(41, 61) - layer_comp_pix[x, y] = ( d,d,d,a[3] ) - if self._tag == "highway" and self._value != "motorway": - d = randrange(140,160) - layer_comp_pix[x, y] = ( d,d,d,a[3] ) - if self._tag == "highway" and self._value == "motorway": - d = randrange(1,20) - r = 86-d - g = 97-d - b = 106-d - layer_comp_pix[x, y] = ( r,g,b,a[3] ) - if self._tag == "waterway" and (self._value == "stream" or self._value == "river"): - d = randrange(1, 15) - # Rock, grass, water - mats = [ (48-d, 45-d, 42-d), (58-d, 81-d, 41-d), (129-d, 148-d, 159-d) ] - # Pick one of those - pick = randrange(1,4) - t = a[3]-d - if t < 0: t = 0 - layer_comp_pix[x, y] = ( mats[pick-1][0], mats[pick-1][1], mats[pick-1][2], t ) - - # A bit special here - if self._tag == "building": - - # Find a color range - d = randrange(1,21) - # Bring in some variety by making the one or other pixel darker - dp = randrange(1, 3) - if dp == 2: - d = d + 20 - # Adjust this pixel - c = (bld_clr[cidx][1]-d, bld_clr[cidx][2]-d, bld_clr[cidx][3]-d, 255) - # Set pixel - layer_comp_pix[x, y] = c - - if self._value == "track" or self._value == "path": - d = randrange(1,20) - r = 164 - d - g = 159 - d - b = 138 - d - layer_comp_pix[x, y] = ( r,g,b,a[3] ) - - # We will do some super magic here to let houses look more realistic - if self._tag == "building": - vls = [ "detached", "hotel", "farm", "semidetached_house", "apartments", "civic", "office", "retail", "industrial", "house", "school", "yes" ] - if self._value in vls: - # Generate a new image - details = Image.new("RGBA", (self._imgsize, self._imgsize)) - details_pix = details.load() - layer_pix = layer_comp.load() - for y in range(self._imgsize-1): - for x in range(self._imgsize-1): - p = layer_pix[x,y] - if p[3] > 0: - shf_x = x+randrange(1, 21) - shf_y = y+randrange(1, 21) - shf_x2 = x-randrange(1, 21) - shf_y2 = y-randrange(1, 21) - if shf_x <= self._imgsize-1 and shf_x >= 0 and shf_y <= self._imgsize-1 and shf_y >= 0: - st = random.uniform(0.65, 0.85) - ca = 255 * st - aa = int(ca) - d = randrange(1,26) - d2 = randrange(1,26) - details_pix[shf_x, shf_y] = (187-d, 179-d, 176-d, aa) - details_pix[shf_x2, shf_y2] = (187-d2, 179-d2, 176-d2, aa) - # Merge the details BELOW the houses - details.alpha_composite(layer_comp) - layer_comp = details - # New edge - osm_edge = osm_mask.filter(ImageFilter.FIND_EDGES) - osm_edge = osm_edge.filter(ImageFilter.MaxFilter) - osm_edge = osm_edge.filter(ImageFilter.GaussianBlur(radius=2)) - # Blur the image - layer_comp = layer_comp.filter(ImageFilter.GaussianBlur(radius=1)) - osm_edge.alpha_composite(layer_comp) - layer_comp = osm_edge - - - # Add some random trees - div = int(self._imgsize/200) - trees = Image.new("RGBA", (self._imgsize, self._imgsize)) - for y in range(0, self._imgsize, div): - for x in range(0, self._imgsize, div): - if x > 0 and x < self._imgsize and y > 0 and y < self._imgsize: - p = mask_pix[x, y] - if p[3] != 0: - # We found something... - # Determine if we put something somewhere - placement = randrange(0, 5) - if placement == 1: - # Do some random shift away from this location - shf_x = randrange(x-11, x+11) - shf_y = randrange(y-11, y+11) - if shf_x > 0 and shf_x < self._imgsize and shf_y > 0 and shf_y < self._imgsize: - # Pick a number of trees to place - numtrees = randrange(1, 16) - for i in range(1, numtrees+1): - # Pick some file - pick = str(randrange(1, 11)) - tree = Image.open(mstr_datafolder + "textures/building/area/p" + pick + ".png") - # Do a correction for the location if needed - if shf_x < 1: shf_x = 1 - if shf_y < 1: shf_y = 1 - if shf_x > self._imgsize - tree.width: shf_x = self._imgsize - tree.width - 1 - if shf_y > self._imgsize - tree.height: shf_y = self._imgsize - tree.height - 1 - trees.alpha_composite(tree, (shf_x, shf_y)) - trees.alpha_composite(layer_comp) - layer_comp = trees - - - mstr_msg("layergen", "Layer image generated") - - # Building shadow - if mstr_shadow_enabled == True: - if self._tag == "building": - mstr_msg("layergen", "Generating shadow for layer") - shadow = Image.new("RGBA", (self._imgsize, self._imgsize)) - shadow_pix = shadow.load() - mask_pix = osm_mask.load() - for y in range(self._imgsize-1): - for x in range(self._imgsize-1): - m = mask_pix[x,y] - shf_x = x + randrange(1, mstr_shadow_shift + 1) - shf_x2 = x + randrange(1, mstr_shadow_shift + 1) - if shf_x <= self._imgsize-1 and shf_x >= 0 and shf_x2 <= self._imgsize-1 and shf_x2 >= 0: - a = mask_pix[x,y][3] - st = random.uniform(0.6, mstr_shadow_strength) - ca = a * st - aa = int(ca) - shadow_pix[shf_x, y] = (0,0,0,aa) - shadow_pix[shf_x2, y] = (0,0,0,aa) - shadow.save(mstr_datafolder + "_cache/" + str(self._latitude) + "-" + str(self._lat_number) + "_" + str(self._longitude) + "-" + str(self._lng_number) + "_" + self._tag + "-" + self._value + "_layer_shadow.png") - mstr_msg("layergen", "Shadow layer completed") - - # Highways and runways of any kind get some special treatment - if (self._tag == "highway" and self._value == "motorway") or (self._tag == "highway" and self._value == "primary") or (self._tag == "highway" and self._value == "secondary") or (self._tag == "highway" and self._value == "tertiary") or (self._tag == "aeroway" and self._value == "runway"): - # We will now add some white lines for coolness - mask_pix = osm_edge.load() - layer_comp_pix = layer_comp.load() - for y in range(self._imgsize): - for x in range(self._imgsize): - if mask_pix[x, y][3] > 0: - # Find a suitable color - w = randrange(185, 215) - a=mask_pix[x,y] - layer_comp_pix[x, y] = ( w,w,w,a[3] ) - - mstr_msg("layergen", "Street lines added") - - if self._tag == "waterway" and (self._value == "river" or self._value == "stream"): - layer_comp = layer_comp.filter(ImageFilter.GaussianBlur(radius=4)) - - # Store layer - layer_comp.save( mstr_datafolder + "_cache/" + str(self._latitude) + "-" + str(self._lat_number) + "_" + str(self._longitude) + "-" + str(self._lng_number) + "_" + self._tag + "-" + self._value + "_layer.png" ) - mstr_msg("layergen", "Layer image finalized and saved.") - - - # Depending on if scenery for XP should be made, AND if normal maps should be made, we would - # need to make them at this exact point - if mstr_xp_genscenery == True: - if mstr_xp_generate_normal_maps == True: - nm = False - for n in mstr_xp_normal_maps: - if n[0] == self._tag and n[1] == self._value: - nm = True - break - if nm == True: - nrm = mstr_xp_normalmap(self._latitude, self._longitude, self._tag, self._value, self._lat_number, self._lng_number, self._latlngfld) - nrm.build_normalmap() - + +# ------------------------------------------------------------------- +# ORTHOGRAPHIC +# Your personal aerial satellite. Always on. At any altitude.* +# Developed by MarStrMind +# License: Open Software License 3.0 +# Up to date version always on marstr.online +# ------------------------------------------------------------------- +# layergen.py +# Generates a full-sized geo layer image, based on the required layer +# type. We use a simple randomization method to generate such an +# image, which is then used for the final photo in photogen. +# ------------------------------------------------------------------- + +import glob +import os +from random import randrange +import random +from PIL import Image, ImageFilter, ImageDraw, ImagePath +from defines import * +from log import * +from tiledb import * +from osmxml import * +from functions import * +from xp_normalmap import * + +class mstr_layergen: + + # Initializes the layer generator. can_choose will go false if we need + # a pre-determined layer from another tile, should this be adjacent to it. + # In this case layer_needed will be populated with the appropriate number. + # You also need the zoom level so that we can generate a scaled version. + def __init__(self, tag, value, lat, latnum, lng, lngnum, is_line, is_completion=False): + self._tag = tag + self._value = value + self._latitude = lat + self._lat_number = latnum + self._longitude = lng + self._lng_number = lngnum + self._layerborder = -1 + self._is_completion = is_completion + # Define layer size depending on what is wanted + self._imgsize = 0 + self._isline = is_line + if mstr_photores == 2048: self._imgsize = 3000 + if mstr_photores == 4096: self._imgsize = 6000 + #mstr_msg("layergen", "Layer gen initialized") + + # Define maximum latitude and longitude tile numbers + def set_max_latlng_tile(self, maxlatlng): + self._maxlat = maxlatlng[0] + self._maxlng = maxlatlng[1] + mstr_msg("layergen", "Maximum latitude and longitude tile numbers received") + + # Set latlng folder + def set_latlng_folder(self, latlngfld): + self._latlngfld = latlngfld + + # Open DB + def open_db(self): + self._tiledb = mstr_tiledb(self._latitude, self._longitude, self._latlngfld) + self._tiledb.create_tables() + + # This generates a "border" image, for example farmland usually has a small space of grass + # before the actual crop of farm field itself. This generates this "border" layer, + # and returns it. + # Needs the actual edge mask, and the tag and value to be used as border. + # Perform necessary adjustments on the mask prior to this call, for example blurring or + # other effects. + def genborder(self, edgemask, tag, value): + layer = Image.new("RGBA", (self._imgsize, self._imgsize)) + root_folder = mstr_datafolder + "textures/" + tag + "/" + value + + # Determine which sources we use + brd = glob.glob(root_folder + "/brd/b*.png") + src = -1 + if len(brd) == 1: src=1 + if len(brd) >= 2: + src = randrange(1, len(brd)+1) + ptc = glob.glob(root_folder + "/ptc/b" + str(src) + "_p*.png") + + # Load in the sources to work with + brd_src = Image.open(root_folder + "/brd/b" + str(src) + ".png") + ptc_src = [] + for p in ptc: + ptc_src.append(Image.open(p)) + mstr_msg("layergen", "Border sources selected") + + # Begin producing a largely random image + samples = 250 # <- We need this in a moment + for i in range(samples): + imgid = 0 + if len(ptc_src) == 1: imgid = 0 + if len(ptc_src) >= 2: + imgid = randrange(1, len(ptc_src)+1) - 1 + l = 0 - int(ptc_src[imgid].width / 2) + r = layer.width - int(ptc_src[imgid].width / 2) + t = 0 - int(ptc_src[imgid].height / 2) + b = layer.height - int(ptc_src[imgid].height / 2) + layer.alpha_composite( ptc_src[imgid], ( randrange(l, r), randrange(t, b) ) ) + mstr_msg("layergen", "Border image generated") + + # We now need to add the seamless border + layer.alpha_composite( brd_src ) + mstr_msg("layergen", "Layer image completed") + + # And now for the Big Mac. + # Generate the layer from the mask. + layer_comp = Image.new("RGBA", (self._imgsize, self._imgsize)) + layer_final = Image.composite(layer, layer_comp, edgemask) + + # Provide the image + return layer_final + + + # This generates the layer from the defined mask + def genlayer(self): + + mstr_msg("layergen", "Layer to be generated: " + str(self._latitude) + "-" + str(self._lat_number) + ":" + str(self._longitude) + "-" + str(self._lng_number) + " -- tag: " + self._tag + " - value: " + self._value ) + + # Before we generate the layer, let's check for airports in this chunk + mstr_msg("layergen", "Checking for airport/s with ICAO code") + osmxml = mstr_osmxml(0,0) + icao = osmxml.find_icao_codes(mstr_datafolder + "_cache/tile.xml") + mstr_msg("layergen", "Found " + str(len(icao)) + " airport/s") + # Runway surface, if any other than concrete/asphalt + rw_surface = "" + # If we find an airport, make a note ... + if len(icao) >= 1: + for i in icao: + # ... but only, if this airport is not already noted + iccheck = self._tiledb.perform_query("SELECT * FROM airports WHERE icao='" + i +"';") + if len(iccheck) == 0: + self._tiledb.insert_icao(i, self._lat_number, self._lng_number, self._latitude, self._longitude) + mstr_msg("layergen", "Airport/s noted in data file") + rw_surface = osmxml.find_runway_surface(mstr_datafolder + "_cache/tile.xml") + + # The image for the layer itself + layer = Image.new("RGBA", (self._imgsize, self._imgsize)) + layer_pix = layer.load() + + # There are some things we need to use sources for, and some things, we do not. + # We need to differentiate that. + + if (self._isline == False and self._tag != "building") or (self._is_completion == True): + # Determine where we get the our source material from + root_folder = mstr_datafolder + "textures/" + for s in mstr_ortho_layers: + if s[0] == self._tag and s[1] == self._value: + fld_main = len(s)-2 + fld_sub = len(s)-1 + root_folder = root_folder + s[fld_main] + "/" + s[fld_sub] + + # Determine which sources to use. + # First, we need to check for adjacent tile information. We then either + # need to use the source of any adjacent tile, or we can choose freely. + src = -1 + + # Find our adjacent tiles + adjtiles = findAdjacentTilesTo(self._lat_number, self._lng_number) + + mstr_msg("layergen", "Performing adjacency check") + # Walk through each tile and see what we can find in relation to this + # tile in the center + # Since we already know the order in adjtiles, we can do this real easy + if self._is_completion == False: + at = self._tiledb.get_adjacency_for_source(adjtiles[0][0], adjtiles[0][1], self._tag, self._value) # Top + ar = self._tiledb.get_adjacency_for_source(adjtiles[1][0], adjtiles[1][1], self._tag, self._value) # Right + ab = self._tiledb.get_adjacency_for_source(adjtiles[2][0], adjtiles[2][1], self._tag, self._value) # Bottom + al = self._tiledb.get_adjacency_for_source(adjtiles[3][0], adjtiles[3][1], self._tag, self._value) # Left + if self._is_completion == True: + at = self._tiledb.get_adjacency_for_completion(adjtiles[0][0], adjtiles[0][1]) # Top + ar = self._tiledb.get_adjacency_for_completion(adjtiles[1][0], adjtiles[1][1]) # Right + ab = self._tiledb.get_adjacency_for_completion(adjtiles[2][0], adjtiles[2][1]) # Bottom + al = self._tiledb.get_adjacency_for_completion(adjtiles[3][0], adjtiles[3][1]) # Left + + if len(at) == 1: + self._tag = at[0][2] + self._value = at[0][3] + if len(ar) == 1: + self._tag = ar[0][2] + self._value = ar[0][3] + if len(ab) == 1: + self._tag = ab[0][2] + self._value = ab[0][3] + if len(al) == 1: + self._tag = al[0][2] + self._value = al[0][3] + + root_folder = mstr_datafolder + "textures/" + for s in mstr_ortho_layers: + if s[0] == self._tag and s[1] == self._value: + fld_main = len(s)-2 + fld_sub = len(s)-1 + root_folder = root_folder + s[fld_main] + "/" + s[fld_sub] + + # We are south to the top tile. + if len(at) == 1 and src == -1: + if "b" in at[0][5]: src = int(at[0][4]) + # We are west to the right tile. + if len(ar) == 1 and src == -1: + if "l" in ar[0][5]: src = int(ar[0][4]) + # We are north to the bottom tile. + if len(ab) == 1 and src == -1: + if "t" in ab[0][5]: src = int(ab[0][4]) + # We are east to the left tile. + if len(al) == 1 and src == -1: + if "r" in al[0][5]: src = int(al[0][4]) + + # Should we be at the border of the degree for latitude and longitude, we need to perform + # additional checks + is_deg_brd_t = False + is_deg_brd_r = False + is_deg_brd_b = False + is_deg_brd_l = False + if self._lat_number == 1: is_deg_brd_b = True + if self._lat_number == self._maxlat: is_deg_brd_t = True + if self._lng_number == 1: is_deg_brd_l = True + if self._lng_number == self._maxlng: is_deg_brd_r = True + + # Adjacent latitude and longitude tiles + deg_tiles = [] + deg_tiles.append( ( self._latitude+1, self._longitude ) ) # Top + deg_tiles.append( ( self._latitude, self._longitude+1 ) ) # Right + deg_tiles.append( ( self._latitude-1, self._longitude ) ) # Bottom + deg_tiles.append( ( self._latitude, self._longitude-1 ) ) # Left + + # Perform degree border checks + # - and make sure we do not run into errors - this drove me crazy in testing + atd = [] + ard = [] + abd = [] + ald = [] + if is_deg_brd_t == True: + if self._is_completion == False: + atd = self._tiledb.get_adjacency_for_source_in_lat_lng(deg_tiles[0][0], deg_tiles[0][1], 1, self._lng_number, self._tag, self._value) # Top + if self._is_completion == True: + atd = self._tiledb.get_adjacency_for_completion_in_lat_lng(deg_tiles[0][0], deg_tiles[0][1], 1, self._lng_number) # Top + if is_deg_brd_r == True: + if self._is_completion == False: + ard = self._tiledb.get_adjacency_for_source_in_lat_lng(deg_tiles[1][0], deg_tiles[1][1], self._lat_number, 1, self._tag, self._value) # Right + if self._is_completion == True: + ard = self._tiledb.get_adjacency_for_completion_in_lat_lng(deg_tiles[1][0], deg_tiles[1][1], self._lat_number, 1) # Right + if is_deg_brd_b == True: + maxlatlng = self._tiledb.get_highest_latlong_from_tile(self._latitude-1, self._longitude) + if self._is_completion == False: + abd = self._tiledb.get_adjacency_for_source_in_lat_lng(deg_tiles[2][0], deg_tiles[2][1], maxlatlng[0], self._lng_number, self._tag, self._value) # Bottom + if self._is_completion == True: + abd = self._tiledb.get_adjacency_for_completion_in_lat_lng(deg_tiles[2][0], deg_tiles[2][1], maxlatlng[0], self._lng_number) # Bottom + if is_deg_brd_l == True: + maxlatlng = self._tiledb.get_highest_latlong_from_tile(self._latitude, self._longitude-1) + if self._is_completion == False: + ald = self._tiledb.get_adjacency_for_source_in_lat_lng(deg_tiles[2][0], deg_tiles[2][1], self._lat_number, maxlatlng[1], self._tag, self._value) # Left + if self._is_completion == True: + ald = self._tiledb.get_adjacency_for_completion_in_lat_lng(deg_tiles[2][0], deg_tiles[2][1], self._lat_number, maxlatlng[1]) # Left + + if (is_deg_brd_t == True or is_deg_brd_r == True or is_deg_brd_b == True or is_deg_brd_l == True): + if src == -1 and self._is_completion == True: + if len(atd) == 1: + self._tag = atd[0][2] + self._value = atd[0][3] + if len(ard) == 1: + self._tag = ard[0][2] + self._value = ard[0][3] + if len(abd) == 1: + self._tag = abd[0][2] + self._value = abd[0][3] + if len(ald) == 1: + self._tag = ald[0][2] + self._value = ald[0][3] + + root_folder = mstr_datafolder + "textures/" + for s in mstr_ortho_layers: + if s[0] == self._tag and s[1] == self._value: + fld_main = len(s)-2 + fld_sub = len(s)-1 + root_folder = root_folder + s[fld_main] + "/" + s[fld_sub] + + + # Should we get here and one of the degree border checks turns out true, + # we need to make sure that we select the source we found. This should + # enable seamless tiling... around the entire planet + if is_deg_brd_t == True and len(at) == 0 and src == -1: + if len(atd) == 1: + if "b" in atd[0][5]: src = int(atd[0][4]) + if is_deg_brd_r == True and len(ar) == 0 and src == -1: + if len(ard) == 1: + if "l" in ard[0][5]: src = int(ard[0][4]) + if is_deg_brd_b == True and len(ab) == 0 and src == -1: + if len(abd) == 1: + if "t" in abd[0][5]: src = int(abd[0][4]) + if is_deg_brd_l == True and len(al) == 0 and src == -1: + if len(ald) == 1: + if "r" in ald[0][5]: src = int(ald[0][4]) + + mstr_msg("layergen", "Adjacency check completed") + + brd = glob.glob(root_folder + "/brd/b*.png") + + # If the adjacency check returned nothing (src is still -1), + # then pick something + if src == -1: + if len(brd) == 1: src=1 + if len(brd) >= 2: + src = randrange(1, len(brd)+1) + + ptc = glob.glob(root_folder + "/ptc/b" + str(src) + "_p*.png") + + # Load in the sources to work with + brd_src = Image.open(root_folder + "/brd/b" + str(src) + ".png") + ptc_src = [] + for p in ptc: + ptc_src.append(Image.open(p)) + mstr_msg("layergen", "Layer sources selected") + + # OK! Load the mask + if self._is_completion == False: + osm_mask = Image.open( mstr_datafolder + "_cache/" + str(self._latitude) + "-" + str(self._lat_number) + "_" + str(self._longitude) + "-" + str(self._lng_number) + "_" + self._tag + "-" + self._value + ".png" ) + if self._is_completion == True: + osm_mask = Image.open( mstr_datafolder + "_cache/" + str(self._latitude) + "-" + str(self._lat_number) + "_" + str(self._longitude) + "-" + str(self._lng_number) + "_tile-completion.png" ) + + # Generate an edge mask from the original + osm_edge = osm_mask.filter(ImageFilter.FIND_EDGES) + osm_edge = osm_edge.filter(ImageFilter.MaxFilter) + mstr_msg("layergen", "Edge mask generated") + + # This adds some natural looking shapes to these types of features + if self._value == "forest" or self._value == "nature_reserve": + epx = osm_edge.load() + imgd = ImageDraw.Draw(osm_mask) + + # Walk through a grid of 200x200 - on the edge image + for y in range(0, osm_mask.height, int(osm_mask.height/200)): + for x in range(0, osm_mask.width, int(osm_mask.width/200)): + px = epx[x,y] + if px[3] == 255: + rx = randrange(24,60) + ry = randrange(24,60) + f = randrange(1,10) + + # Randomize the found locations a little + psx = randrange(x-11, x+11) + psy = randrange(y-11, y+11) + + # Do some magic - but not on edges + if x > 0 and x < osm_mask.width and y > 0 and y < osm_mask.height: + if f != 5: + imgd.ellipse((psx-int(rx/2), psy-int(ry/2), psx+rx, psy+ry), fill="black") + if f == 3 or f == 7: + imgd.ellipse((psx-int(rx/2), psy-int(ry/2), psx+rx, psy+ry), fill=(0,0,0,0)) + + + # We need to change the image in certain conditions + if self._value == "hedge" and self._tag == "barrier": + osm_mask = osm_edge + + # From here on in we will need to perform some adjustments on the masks, depending + # on what they are. + for i in mstr_mask_blur: + if i[0] == self._tag and i[1] == self._value: + if self._tag != "place" and (self._value != "sea" or self._value != "ocean"): + osm_mask = osm_mask.filter(ImageFilter.BoxBlur(radius=i[2])) + break + + # Begin producing a largely random image + samples = 250 # <- We need this in a moment + for i in range(samples): + imgid = 0 + if len(ptc_src) == 1: imgid = 0 + if len(ptc_src) >= 2: + imgid = randrange(1, len(ptc_src)+1) - 1 + l = 0 - int(ptc_src[imgid].width / 2) + r = layer.width - int(ptc_src[imgid].width / 2) + t = 0 - int(ptc_src[imgid].height / 2) + b = layer.height - int(ptc_src[imgid].height / 2) + layer.alpha_composite( ptc_src[imgid], ( randrange(l, r), randrange(t, b) ) ) + mstr_msg("layergen", "Layer image generated") + + + # Here we need to do some magic to make some features look more natural + if (self._tag == "landuse" and self._value == "meadow") or (self._tag == "natural" and self._value == "grassland") or (self._tag == "natural" and self._value == "heath"): + amt = randrange(1,5) + for i in range(1, amt+1): + ptc = randrange(1, 14) + img = Image.open(mstr_datafolder + "textures/tile/completion/p" + str(ptc)+".png") + lx = randrange( int(layer.width/20), layer.width - (int(layer.width/20)) - img.width ) + ly = randrange( int(layer.width/20), layer.width - (int(layer.width/20)) - img.height ) + layer.alpha_composite( img, (lx, ly) ) + + + # We now need to add the seamless border + layer.alpha_composite( brd_src ) + mstr_msg("layergen", "Layer image completed") + + + # And now for the Big Mac. + # Generate the layer from the mask. + layer_comp = Image.new("RGBA", (self._imgsize, self._imgsize)) + layer_pix = layer.load() + mask_pix = osm_mask.load() + layer_comp_pix = layer_comp.load() + for y in range(self._imgsize): + for x in range(self._imgsize): + if mask_pix[x, y][3] > 0: + rgb=layer_pix[x,y] + a=mask_pix[x,y] + if self._value == "residential": + layer_comp_pix[x, y] = ( rgb[0], rgb[1], rgb[2], int(a[3]/2)) + if self._value != "residential": + layer_comp_pix[x, y] = ( rgb[0], rgb[1], rgb[2], a[3]) + + # For some things, we will need to add a border and then add this to the layer. + layer_border = None + if self._tag == "landuse": + if self._value == "forest" or self._value == "farmland": + osm_edge = osm_edge.filter(ImageFilter.ModeFilter(size=15)) + osm_edge = osm_edge.filter(ImageFilter.BoxBlur(radius=2)) + layer_border = self.genborder(osm_edge, "landuse", "meadow") + layer_comp.alpha_composite(layer_border) + + # Store layer + if self._is_completion == False: + layer_comp.save( mstr_datafolder + "_cache/" + str(self._latitude) + "-" + str(self._lat_number) + "_" + str(self._longitude) + "-" + str(self._lng_number) + "_" + self._tag + "-" + self._value + "_layer.png" ) + if self._is_completion == True: + layer_comp.save( mstr_datafolder + "_cache/" + str(self._latitude) + "-" + str(self._lat_number) + "_" + str(self._longitude) + "-" + str(self._lng_number) + "_tile-completion_layer.png" ) + #layer_final.save( mstr_datafolder + "_cache/" + str(self._latitude) + "-" + str(self._lat_number) + "_" + str(self._longitude) + "-" + str(self._lng_number) + "_" + self._tag + "-" + self._value + "_layer.png" ) + mstr_msg("layergen", "Layer image finalized and saved.") + + + # Depending on if scenery for XP should be made, AND if normal maps should be made, we would + # need to make them at this exact point + if mstr_xp_genscenery == True: + if mstr_xp_generate_normal_maps == True: + nm = False + for n in mstr_xp_normal_maps: + if n[0] == self._tag and n[1] == self._value: + nm = True + break + if nm == True: + nrm = mstr_xp_normalmap(self._latitude, self._longitude, self._tag, self._value, self._lat_number, self._lng_number, self._latlngfld) + nrm.build_normalmap() + + + # Let's try our hand at pseudo shadows + if mstr_shadow_enabled == True: + if mstr_shadow_shift >= 2: + shadow = Image.new("RGBA", (self._imgsize, self._imgsize)) + for sh in mstr_shadow_casters: + if self._tag == sh[0] and self._value == sh[1]: + mstr_msg("layergen", "Generating shadow for layer") + shadow_pix = shadow.load() + mask_pix = osm_mask.load() + for y in range(self._imgsize-1): + for x in range(self._imgsize-1): + m = mask_pix[x,y] + shf_x = 0 + # Buildings get slightly closer shadows + if self._tag == "building": + shf_x = x + int(mstr_shadow_shift/2) + if self._tag != "building": + shf_x = x + mstr_shadow_shift + if shf_x <= self._imgsize-1: + a = mask_pix[x,y][3] + st = 0 + if self._tag == "building": + st = random.uniform(0.25, mstr_shadow_strength/2) + if self._tag != "building": + st = random.uniform(0.45, mstr_shadow_strength) + ca = a * st + aa = int(ca) + shadow_pix[shf_x, y] = (0,0,0,aa) + shadow.save(mstr_datafolder + "_cache/" + str(self._latitude) + "-" + str(self._lat_number) + "_" + str(self._longitude) + "-" + str(self._lng_number) + "_" + self._tag + "-" + self._value + "_layer_shadow.png") + mstr_msg("layergen", "Shadow layer completed") + + + + # Check if pixels touch the borders of the image, and if so - + # make a not of that in the database. + at=False + ar=False + ab=False + al=False + layer_pix = layer_comp.load() # <- Just to be safe + + # Top scan + for i in range(0, self._imgsize-1): + p = layer_pix[i,0] + if p[3] > 0: + at=True + break + + # Right scan + for i in range(0, self._imgsize-1): + p = layer_pix[self._imgsize-1,i] + if p[3] > 0: + ar=True + break + + # Bottom scan + for i in range(0, self._imgsize-1): + p = layer_pix[i,self._imgsize-1] + if p[3] > 0: + ab=True + break + + # Left scan + for i in range(0, self._imgsize-1): + p = layer_pix[1,i] + if p[3] > 0: + al=True + break + + # Construct DB String + adjstr = "" + if at==True: adjstr = adjstr + "t" + if ar==True: adjstr = adjstr + "r" + if ab==True: adjstr = adjstr + "b" + if al==True: adjstr = adjstr + "l" + + # Store into DB - but only if there is something to store + if adjstr != "": + if self._is_completion == False: + r = self._tiledb.get_adjacency_for_source(self._lat_number, self._lng_number, self._tag, self._value) + if len(r) == 0: + self._tiledb.insert_info(self._lat_number, self._lng_number, self._tag, self._value, src, adjstr) + mstr_msg("layergen", "Adjacency info stored in database") + + if self._is_completion == True: + r = self._tiledb.get_adjacency_for_completion(self._lat_number, self._lng_number) + if len(r) == 0: + self._tiledb.insert_completion_info(self._lat_number, self._lng_number, self._tag, self._value, src, adjstr) + mstr_msg("layergen", "Adjacency info for completion stored in database") + + self._tiledb.commit_query() + self._tiledb.close_db() + + + # If we encounter one of these road-specific tags, we need to proceed differently. + + if self._isline == True or self._tag == "building": + + # We will need the mask in question + osm_mask = Image.open( mstr_datafolder + "_cache/" + str(self._latitude) + "-" + str(self._lat_number) + "_" + str(self._longitude) + "-" + str(self._lng_number) + "_" + self._tag + "-" + self._value + ".png" ) + + # Generate an edge mask from the original + osm_edge = osm_mask.filter(ImageFilter.FIND_EDGES) + osm_edge = osm_edge.filter(ImageFilter.MaxFilter) + mstr_msg("layergen", "Edge mask generated") + + # As above, we will apply the blur as noted in the defines + for i in mstr_mask_blur: + if i[0] == self._tag and i[1] == self._value: + osm_mask = osm_mask.filter(ImageFilter.BoxBlur(radius=i[2])) + break + osm_edge = osm_edge.filter(ImageFilter.BoxBlur(radius=1)) + + + # And now for the Big Mac. + # Generate the layer from the mask. Same as above - except! + # This time we have no source material - instead we will fill the + # mask with a color that is appropriate for this street type. + layer_comp = Image.new("RGBA", (self._imgsize, self._imgsize)) + mask_pix = osm_mask.load() + edge_pix = osm_edge.load() + layer_comp_pix = layer_comp.load() + + # Let's define some base color ranges for different types of buildings + bld_clr = [ + ("detached", 190, 192, 195), + ("church", 134, 134, 136), + ("hotel", 153, 147, 138), + ("farm", 145, 124, 121), + ("semidetached_house", 167, 163, 152), + ("apartments", 129, 134, 127), + ("civic", 134, 134, 136), + ("garage", 101, 109, 111), + ("office", 139, 152, 156), + ("retail", 121, 122, 108), + ("industrial", 191, 192, 187), + ("house", 145, 124, 121), + ("terrace", 191, 192, 187), + ("hangar", 137, 162, 195), + ("school", 111, 117, 115), + ("yes", 152, 144, 141) + ] + + # Find the color index to work with + cidx = 0 + if self._tag == "building": + for c in bld_clr: + if c[0] == self._value: + break + cidx = cidx+1 + + for y in range(self._imgsize): + for x in range(self._imgsize): + if mask_pix[x, y][3] > 0: + a=mask_pix[x,y] + e=edge_pix[x,y] + # Find a suitable color + d = 0 + if self._tag == "aeroway" and self._value == "runway": + # It seems only runways with any other surface than concrete + # are mentioned in OSM. So we need to make sure when to render + # "concrete" and when to leave it. Only sometimes the word + # "asphalt" is mentioned + if rw_surface == "" or rw_surface == "asphalt": + d = randrange(81, 101) + layer_comp_pix[x, y] = ( d,d,d,a[3] ) + if self._tag == "railway": + d = randrange(41, 61) + layer_comp_pix[x, y] = ( d,d,d,a[3] ) + if self._tag == "highway" and self._value != "motorway": + d = randrange(140,160) + layer_comp_pix[x, y] = ( d,d,d,a[3] ) + if self._tag == "highway" and self._value == "motorway": + d = randrange(1,20) + r = 86-d + g = 97-d + b = 106-d + layer_comp_pix[x, y] = ( r,g,b,a[3] ) + if self._tag == "waterway" and (self._value == "stream" or self._value == "river"): + d = randrange(1, 15) + # Rock, grass, water + mats = [ (48-d, 45-d, 42-d), (58-d, 81-d, 41-d), (129-d, 148-d, 159-d) ] + # Pick one of those + pick = randrange(1,4) + t = a[3]-d + if t < 0: t = 0 + layer_comp_pix[x, y] = ( mats[pick-1][0], mats[pick-1][1], mats[pick-1][2], t ) + + # A bit special here + if self._tag == "building": + + # Find a color range + d = randrange(1,21) + # Bring in some variety by making the one or other pixel darker + dp = randrange(1, 3) + if dp == 2: + d = d + 20 + # Adjust this pixel + c = (bld_clr[cidx][1]-d, bld_clr[cidx][2]-d, bld_clr[cidx][3]-d, 255) + # Set pixel + layer_comp_pix[x, y] = c + + if self._value == "track" or self._value == "path": + d = randrange(1,20) + r = 164 - d + g = 159 - d + b = 138 - d + layer_comp_pix[x, y] = ( r,g,b,a[3] ) + + # We will do some super magic here to let houses look more realistic + if self._tag == "building": + vls = [ "detached", "hotel", "farm", "semidetached_house", "apartments", "civic", "office", "retail", "industrial", "house", "school", "yes" ] + if self._value in vls: + # Generate a new image + details = Image.new("RGBA", (self._imgsize, self._imgsize)) + details_pix = details.load() + layer_pix = layer_comp.load() + for y in range(self._imgsize-1): + for x in range(self._imgsize-1): + p = layer_pix[x,y] + if p[3] > 0: + shf_x = x+randrange(1, 21) + shf_y = y+randrange(1, 21) + shf_x2 = x-randrange(1, 21) + shf_y2 = y-randrange(1, 21) + if shf_x <= self._imgsize-1 and shf_x >= 0 and shf_y <= self._imgsize-1 and shf_y >= 0: + st = random.uniform(0.65, 0.85) + ca = 255 * st + aa = int(ca) + d = randrange(1,26) + d2 = randrange(1,26) + details_pix[shf_x, shf_y] = (187-d, 179-d, 176-d, aa) + details_pix[shf_x2, shf_y2] = (187-d2, 179-d2, 176-d2, aa) + # Merge the details BELOW the houses + details.alpha_composite(layer_comp) + layer_comp = details + # New edge + osm_edge = osm_mask.filter(ImageFilter.FIND_EDGES) + osm_edge = osm_edge.filter(ImageFilter.MaxFilter) + osm_edge = osm_edge.filter(ImageFilter.GaussianBlur(radius=2)) + # Blur the image + layer_comp = layer_comp.filter(ImageFilter.GaussianBlur(radius=1)) + osm_edge.alpha_composite(layer_comp) + layer_comp = osm_edge + + + # Add some random trees + div = int(self._imgsize/200) + trees = Image.new("RGBA", (self._imgsize, self._imgsize)) + for y in range(0, self._imgsize, div): + for x in range(0, self._imgsize, div): + if x > 0 and x < self._imgsize and y > 0 and y < self._imgsize: + p = mask_pix[x, y] + if p[3] != 0: + # We found something... + # Determine if we put something somewhere + placement = randrange(0, 5) + if placement == 1: + # Do some random shift away from this location + shf_x = randrange(x-11, x+11) + shf_y = randrange(y-11, y+11) + if shf_x > 0 and shf_x < self._imgsize and shf_y > 0 and shf_y < self._imgsize: + # Pick a number of trees to place + numtrees = randrange(1, 16) + for i in range(1, numtrees+1): + # Pick some file + pick = str(randrange(1, 11)) + tree = Image.open(mstr_datafolder + "textures/building/area/p" + pick + ".png") + # Do a correction for the location if needed + if shf_x < 1: shf_x = 1 + if shf_y < 1: shf_y = 1 + if shf_x > self._imgsize - tree.width: shf_x = self._imgsize - tree.width - 1 + if shf_y > self._imgsize - tree.height: shf_y = self._imgsize - tree.height - 1 + trees.alpha_composite(tree, (shf_x, shf_y)) + trees.alpha_composite(layer_comp) + layer_comp = trees + + + mstr_msg("layergen", "Layer image generated") + + # Building shadow + if mstr_shadow_enabled == True: + if self._tag == "building": + mstr_msg("layergen", "Generating shadow for layer") + shadow = Image.new("RGBA", (self._imgsize, self._imgsize)) + shadow_pix = shadow.load() + mask_pix = osm_mask.load() + for y in range(self._imgsize-1): + for x in range(self._imgsize-1): + m = mask_pix[x,y] + shf_x = x + randrange(1, mstr_shadow_shift + 1) + shf_x2 = x + randrange(1, mstr_shadow_shift + 1) + if shf_x <= self._imgsize-1 and shf_x >= 0 and shf_x2 <= self._imgsize-1 and shf_x2 >= 0: + a = mask_pix[x,y][3] + st = random.uniform(0.6, mstr_shadow_strength) + ca = a * st + aa = int(ca) + shadow_pix[shf_x, y] = (0,0,0,aa) + shadow_pix[shf_x2, y] = (0,0,0,aa) + shadow.save(mstr_datafolder + "_cache/" + str(self._latitude) + "-" + str(self._lat_number) + "_" + str(self._longitude) + "-" + str(self._lng_number) + "_" + self._tag + "-" + self._value + "_layer_shadow.png") + mstr_msg("layergen", "Shadow layer completed") + + # Highways and runways of any kind get some special treatment + if (self._tag == "highway" and self._value == "motorway") or (self._tag == "highway" and self._value == "primary") or (self._tag == "highway" and self._value == "secondary") or (self._tag == "highway" and self._value == "tertiary") or (self._tag == "aeroway" and self._value == "runway"): + # We will now add some white lines for coolness + mask_pix = osm_edge.load() + layer_comp_pix = layer_comp.load() + for y in range(self._imgsize): + for x in range(self._imgsize): + if mask_pix[x, y][3] > 0: + # Find a suitable color + w = randrange(185, 215) + a=mask_pix[x,y] + layer_comp_pix[x, y] = ( w,w,w,a[3] ) + + mstr_msg("layergen", "Street lines added") + + if self._tag == "waterway" and (self._value == "river" or self._value == "stream"): + layer_comp = layer_comp.filter(ImageFilter.GaussianBlur(radius=4)) + + # Store layer + layer_comp.save( mstr_datafolder + "_cache/" + str(self._latitude) + "-" + str(self._lat_number) + "_" + str(self._longitude) + "-" + str(self._lng_number) + "_" + self._tag + "-" + self._value + "_layer.png" ) + mstr_msg("layergen", "Layer image finalized and saved.") + + + # Depending on if scenery for XP should be made, AND if normal maps should be made, we would + # need to make them at this exact point + if mstr_xp_genscenery == True: + if mstr_xp_generate_normal_maps == True: + nm = False + for n in mstr_xp_normal_maps: + if n[0] == self._tag and n[1] == self._value: + nm = True + break + if nm == True: + nrm = mstr_xp_normalmap(self._latitude, self._longitude, self._tag, self._value, self._lat_number, self._lng_number, self._latlngfld) + nrm.build_normalmap() + diff --git a/log.py b/log.py index e7ad68a..2a26dd5 100644 --- a/log.py +++ b/log.py @@ -1,19 +1,19 @@ - -# ------------------------------------------------------------------- -# ORTHOGRAPHIC -# Your personal aerial satellite. Always on. At any altitude.* -# Developed by MarStrMind -# License: Open Software License 3.0 -# Up to date version always on marstr.online -# ------------------------------------------------------------------- -# log.py -# Convenience call to log output -# ------------------------------------------------------------------- - -import datetime -from defines import * - -def mstr_msg(fnc, msg): - if mstr_show_log == True: - now = datetime.datetime.now() + +# ------------------------------------------------------------------- +# ORTHOGRAPHIC +# Your personal aerial satellite. Always on. At any altitude.* +# Developed by MarStrMind +# License: Open Software License 3.0 +# Up to date version always on marstr.online +# ------------------------------------------------------------------- +# log.py +# Convenience call to log output +# ------------------------------------------------------------------- + +import datetime +from defines import * + +def mstr_msg(fnc, msg): + if mstr_show_log == True: + now = datetime.datetime.now() print(now.strftime(" %H:%M:%S" + " | ["+fnc+"] | " + msg)) \ No newline at end of file diff --git a/maskgen.py b/maskgen.py index c315360..7063729 100644 --- a/maskgen.py +++ b/maskgen.py @@ -1,192 +1,192 @@ - -# ------------------------------------------------------------------- -# ORTHOGRAPHIC -# Your personal aerial satellite. Always on. At any altitude.* -# Developed by MarStrMind -# License: Open Software License 3.0 -# Up to date version always on marstr.online -# ------------------------------------------------------------------- -# maskgen.py -# The class that generates a mask of the layer it was asked to do. -# This mask will then be used to generate a photo layer, which in -# turn is then used to construct the final photo. It can be argued -# that this part of the code is the most crucial one, as the other -# classes involved rely on what this code is doing, and by extension, -# generating. -# -# The PNG generated will be used in this progression: -# - Generate mask from OSM (here) -# - Generate colored photo layer from this mask, for example for -# landuse: forest -# - Compile actual satellite aerial -# ------------------------------------------------------------------- - -import math -from osmxml import * -from defines import * -from log import * -from PIL import Image, ImageFilter, ImageDraw, ImagePath -from random import randrange -from tiledb import * -import random - -class mstr_maskgen: - - # Initializes the class with some required variables - # Much of this code is adjusted to work within a class. - def __init__(self, box, vstep, tag, value, isline, subtag=None, subvalue=None): - self._box = box - self._tag = tag - self._subtag = subtag - self._subvalue = subvalue - self._value = value - self._vstep = vstep - self._scale = 1 / math.cos(math.radians(self._box[0])) - self._isline = isline - - #mstr_msg("maskgen", "Intialized mask gen.") - - - # Projects a point into the canvas of the mask. - # Final projection depends on positive or negative latitude or longitude. - def project_pixel(self, pnt, edge): - pdiff = edge - pnt - byT = pdiff * 1000 - divisor = byT / 16 - return divisor - - - # Extract lat/lng from custom extracted nodes block - def latlong_from_id(self, id, nds): - latlng = [] - for i in nds: - if i[0] == id: - #latlng.append((float(i[1]), float(i[2]))) - latlng.append(float(i[1])) - latlng.append(float(i[2])) - break - return latlng - - - # Only needed if X-Plane scenery is built - def _set_xpscenery_datagroup(self, dg): - self._xpdg = dg - - - # Builds the required mask - def _build_mask(self): - # Generate empty image - imgsize = 0 - if mstr_photores == 2048: imgsize=3000 - if mstr_photores == 4096: imgsize=6000 - mask_img = Image.new("RGBA", (imgsize, imgsize)) - - tilexml = mstr_datafolder + "_cache/tile.xml" - xml = mstr_osmxml(0,0) - fstr = str(self._box[0]) + "-" + str(self._box[1]) + "_" + str(self._box[2]) + "-" + str(self._box[3]) - nds = xml.acquire_nodes(tilexml) - way = xml.acquire_waypoint_data(tilexml) - rls = xml.acquire_relations(tilexml) - - mstr_msg("maskgen", "Building mask for " + str(self._box[0]) + "-" + str(self._box[1]) + ", " + str(self._box[2]) + "-" + str(self._box[3]) + ", for " + self._tag + ": " + self._value ) - - frs = [] - - # Calculate actual bounding box - bbox = [] - # Latitude - bbox.append(self._box[0] + ((self._box[1]-1) * self._vstep)) - bbox.append(self._box[0] + ((self._box[1]-1) * self._vstep) + self._vstep) - # Longitude - bbox.append(self._box[2] + ((self._box[3]-1) * mstr_zl_18)) - bbox.append(self._box[2] + ((self._box[3]-1) * mstr_zl_18) + mstr_zl_18) - - # Generate mask for ONE tag only - if self._subtag == None: - for w in way: - if w[2] == self._tag and w[3] == self._value: - nd = [] - for d in way: - if d[0] == w[0]: - nd.append(d[1]) - frs.append(nd) - # Scout through relations as these also make up map data - for r in rls: - if self._tag in r[1] and self._value in r[1]: - nd = [] - for w in way: - if int(w[0]) == int(r[0]): - nd.append(w[1]) - frs.append(nd) - - # Generate mask for one tag, PLUS a subtag. This is mostly used for admin areas - if self._subtag != None: - nd = [] - wids = [] - for w in way: - if w[2] == self._tag and w[3] == self._value: - wids.append(w[0]) - for w in wids: - for wp in way: - if wp[0] == w and wp[2] == self._subtag and wp[3] in self._subvalue: - for d in way: - if d[0] == wp[0] and d[1] != "NULL": - nd.append(d[1]) - frs.append(nd) - - # Project all pixels - for f in frs: - pts = [] - for a in f: - latlng = self.latlong_from_id(a, nds) - if len(latlng) == 2: - # For some reason, sometimes the array is empty. Make sure we have two data points. - if len(latlng) == 2: - - # Project the pixel, and add to the polygon shape. - p_lat = self.project_pixel(latlng[0], bbox[1]) - p_lng = self.project_pixel(latlng[1], bbox[3]) - pixlat = 0 - pixlng = 0 - pr = 0 - if mstr_photores == 2048: pr = 3000 - if mstr_photores == 4096: pr = 6000 - - # Draw pixels in direction according to latitude and longitude positions - - - # Latitude: - if self._box[0] > 0: - pixlat = int((imgsize*self._scale)*p_lat) - if self._box[0] < 0: - pixlat = pr - (int((imgsize*self._scale)*p_lat)) - - # Longitude: - if self._box[2] > 0: - pixlng = int(imgsize - (imgsize*p_lng)) - if self._box[2] < 0: - pixlng = pr - (int(imgsize - (imgsize*p_lng))) - - pts.append((pixlng, pixlat)) - - # Corel Draw! - imgd = ImageDraw.Draw(mask_img) - - # Draw polygons for everything except those three tags - if self._isline == False: - if len(pts) >= 3: - imgd.polygon(pts, fill="#000000") - - # For road specific items, draw lines instead - if self._isline == True: - if len(pts) >= 2: # Only need two points to form a line - idx = 0 - for i in range(len(mstr_ortho_layers)): - if mstr_ortho_layers[i][0] == self._tag and mstr_ortho_layers[i][1] == self._value: - idx = i - break - imgd.line(pts, fill="#000000", width=mstr_ortho_layers[idx][2], joint="curve") - - # Save image - mask_img.save(mstr_datafolder + "_cache/" + fstr + "_" + self._tag + "-" + self._value + ".png") - # Inform - mstr_msg("maskgen", "Mask built.") + +# ------------------------------------------------------------------- +# ORTHOGRAPHIC +# Your personal aerial satellite. Always on. At any altitude.* +# Developed by MarStrMind +# License: Open Software License 3.0 +# Up to date version always on marstr.online +# ------------------------------------------------------------------- +# maskgen.py +# The class that generates a mask of the layer it was asked to do. +# This mask will then be used to generate a photo layer, which in +# turn is then used to construct the final photo. It can be argued +# that this part of the code is the most crucial one, as the other +# classes involved rely on what this code is doing, and by extension, +# generating. +# +# The PNG generated will be used in this progression: +# - Generate mask from OSM (here) +# - Generate colored photo layer from this mask, for example for +# landuse: forest +# - Compile actual satellite aerial +# ------------------------------------------------------------------- + +import math +from osmxml import * +from defines import * +from log import * +from PIL import Image, ImageFilter, ImageDraw, ImagePath +from random import randrange +from tiledb import * +import random + +class mstr_maskgen: + + # Initializes the class with some required variables + # Much of this code is adjusted to work within a class. + def __init__(self, box, vstep, tag, value, isline, subtag=None, subvalue=None): + self._box = box + self._tag = tag + self._subtag = subtag + self._subvalue = subvalue + self._value = value + self._vstep = vstep + self._scale = 1 / math.cos(math.radians(self._box[0])) + self._isline = isline + + #mstr_msg("maskgen", "Intialized mask gen.") + + + # Projects a point into the canvas of the mask. + # Final projection depends on positive or negative latitude or longitude. + def project_pixel(self, pnt, edge): + pdiff = edge - pnt + byT = pdiff * 1000 + divisor = byT / 16 + return divisor + + + # Extract lat/lng from custom extracted nodes block + def latlong_from_id(self, id, nds): + latlng = [] + for i in nds: + if i[0] == id: + #latlng.append((float(i[1]), float(i[2]))) + latlng.append(float(i[1])) + latlng.append(float(i[2])) + break + return latlng + + + # Only needed if X-Plane scenery is built + def _set_xpscenery_datagroup(self, dg): + self._xpdg = dg + + + # Builds the required mask + def _build_mask(self): + # Generate empty image + imgsize = 0 + if mstr_photores == 2048: imgsize=3000 + if mstr_photores == 4096: imgsize=6000 + mask_img = Image.new("RGBA", (imgsize, imgsize)) + + tilexml = mstr_datafolder + "_cache/tile.xml" + xml = mstr_osmxml(0,0) + fstr = str(self._box[0]) + "-" + str(self._box[1]) + "_" + str(self._box[2]) + "-" + str(self._box[3]) + nds = xml.acquire_nodes(tilexml) + way = xml.acquire_waypoint_data(tilexml) + rls = xml.acquire_relations(tilexml) + + mstr_msg("maskgen", "Building mask for " + str(self._box[0]) + "-" + str(self._box[1]) + ", " + str(self._box[2]) + "-" + str(self._box[3]) + ", for " + self._tag + ": " + self._value ) + + frs = [] + + # Calculate actual bounding box + bbox = [] + # Latitude + bbox.append(self._box[0] + ((self._box[1]-1) * self._vstep)) + bbox.append(self._box[0] + ((self._box[1]-1) * self._vstep) + self._vstep) + # Longitude + bbox.append(self._box[2] + ((self._box[3]-1) * mstr_zl_18)) + bbox.append(self._box[2] + ((self._box[3]-1) * mstr_zl_18) + mstr_zl_18) + + # Generate mask for ONE tag only + if self._subtag == None: + for w in way: + if w[2] == self._tag and w[3] == self._value: + nd = [] + for d in way: + if d[0] == w[0]: + nd.append(d[1]) + frs.append(nd) + # Scout through relations as these also make up map data + for r in rls: + if self._tag in r[1] and self._value in r[1]: + nd = [] + for w in way: + if int(w[0]) == int(r[0]): + nd.append(w[1]) + frs.append(nd) + + # Generate mask for one tag, PLUS a subtag. This is mostly used for admin areas + if self._subtag != None: + nd = [] + wids = [] + for w in way: + if w[2] == self._tag and w[3] == self._value: + wids.append(w[0]) + for w in wids: + for wp in way: + if wp[0] == w and wp[2] == self._subtag and wp[3] in self._subvalue: + for d in way: + if d[0] == wp[0] and d[1] != "NULL": + nd.append(d[1]) + frs.append(nd) + + # Project all pixels + for f in frs: + pts = [] + for a in f: + latlng = self.latlong_from_id(a, nds) + if len(latlng) == 2: + # For some reason, sometimes the array is empty. Make sure we have two data points. + if len(latlng) == 2: + + # Project the pixel, and add to the polygon shape. + p_lat = self.project_pixel(latlng[0], bbox[1]) + p_lng = self.project_pixel(latlng[1], bbox[3]) + pixlat = 0 + pixlng = 0 + pr = 0 + if mstr_photores == 2048: pr = 3000 + if mstr_photores == 4096: pr = 6000 + + # Draw pixels in direction according to latitude and longitude positions - + + # Latitude: + if self._box[0] > 0: + pixlat = int((imgsize*self._scale)*p_lat) + if self._box[0] < 0: + pixlat = pr - (int((imgsize*self._scale)*p_lat)) + + # Longitude: + if self._box[2] > 0: + pixlng = int(imgsize - (imgsize*p_lng)) + if self._box[2] < 0: + pixlng = pr - (int(imgsize - (imgsize*p_lng))) + + pts.append((pixlng, pixlat)) + + # Corel Draw! + imgd = ImageDraw.Draw(mask_img) + + # Draw polygons for everything except those three tags + if self._isline == False: + if len(pts) >= 3: + imgd.polygon(pts, fill="#000000") + + # For road specific items, draw lines instead + if self._isline == True: + if len(pts) >= 2: # Only need two points to form a line + idx = 0 + for i in range(len(mstr_ortho_layers)): + if mstr_ortho_layers[i][0] == self._tag and mstr_ortho_layers[i][1] == self._value: + idx = i + break + imgd.line(pts, fill="#000000", width=mstr_ortho_layers[idx][2], joint="curve") + + # Save image + mask_img.save(mstr_datafolder + "_cache/" + fstr + "_" + self._tag + "-" + self._value + ".png") + # Inform + mstr_msg("maskgen", "Mask built.") diff --git a/og.py b/og.py index 90a469e..a115f01 100644 --- a/og.py +++ b/og.py @@ -1,52 +1,52 @@ - -import sys -import os -from orthographic import * -from log import * -from defines import * - -# ------------------------------------------------------------------- -# ORTHOGRAPHIC -# Your personal aerial satellite. Always on. At any altitude.* -# Developed by MarStrMind -# License: Open Software License 3.0 -# Up to date version always on marstr.online -# ------------------------------------------------------------------- -# og.py -# Main file to call to generate an ortho tile; entry point to tool. -# ------------------------------------------------------------------- - - -# Print a welcome message -print(" ") -print(" ---------------------------------------------------------------- ") -print(" ORTHOGRAPHIC: An ortho-photo generator, using real world data.") -print(" Developed by MarStrMind - Code available on marstr.online") -print(" ---------------------------------------------------------------- ") -print(" ") - - -# Evaluate CLI arguments and process tile. - -cli = False -if len(sys.argv) == 3: - cli = True - -# Only if we find enough arguments, proceed. -if cli == True: - lat = int(sys.argv[1]) - lng = int(sys.argv[2]) - - mstr_msg("_main", "Beginning tile generation process.") - - # Create the class and init values - og = mstr_orthographic(lat, lng, mstr_datafolder, os.getcwd()) - og._buildTile() - -else: - mstr_msg("_main", "Please provide Latitude and Longitude. Exiting.") - print ("") - - - + +import sys +import os +from orthographic import * +from log import * +from defines import * + +# ------------------------------------------------------------------- +# ORTHOGRAPHIC +# Your personal aerial satellite. Always on. At any altitude.* +# Developed by MarStrMind +# License: Open Software License 3.0 +# Up to date version always on marstr.online +# ------------------------------------------------------------------- +# og.py +# Main file to call to generate an ortho tile; entry point to tool. +# ------------------------------------------------------------------- + + +# Print a welcome message +print(" ") +print(" ---------------------------------------------------------------- ") +print(" ORTHOGRAPHIC: An ortho-photo generator, using real world data.") +print(" Developed by MarStrMind - Code available on marstr.online") +print(" ---------------------------------------------------------------- ") +print(" ") + + +# Evaluate CLI arguments and process tile. + +cli = False +if len(sys.argv) == 3: + cli = True + +# Only if we find enough arguments, proceed. +if cli == True: + lat = int(sys.argv[1]) + lng = int(sys.argv[2]) + + mstr_msg("_main", "Beginning tile generation process.") + + # Create the class and init values + og = mstr_orthographic(lat, lng, mstr_datafolder, os.getcwd()) + og._buildTile() + +else: + mstr_msg("_main", "Please provide Latitude and Longitude. Exiting.") + print ("") + + + # * No Space Shuttle or SpaceShipOne needed to deploy. \ No newline at end of file diff --git a/orthographic.py b/orthographic.py index c692a16..c71f325 100644 --- a/orthographic.py +++ b/orthographic.py @@ -1,346 +1,346 @@ - -# ------------------------------------------------------------------- -# ORTHOGRAPHIC -# Your personal aerial satellite. Always on. At any altitude.* -# Developed by MarStrMind -# License: Open Software License 3.0 -# Up to date version always on marstr.online -# ------------------------------------------------------------------- -# orthographic.py -# Main class which handles the generation of the ortho tile. -# ------------------------------------------------------------------- - -import math -import os -import glob -from defines import * -from log import * -from maskgen import * -from layergen import * -from photogen import * -from osmxml import * -from tilegen import * -from xp_dsfgen import * - - -# The main class which handles the rest -class mstr_orthographic: - - # Constructor of class. Takes longitude and latitude. - def __init__(self, lat, lng, outfolder, pwd): - self._lat = lat - self._long = lng - self._output = outfolder - self._pwd = pwd - self._vstep = self._findVerticalStepping() - self._latlngfld = self.latlng_folder([lat,lng]) - mstr_msg("orthographic", "Initiated with LAT: " + str(lat) + ", LNG: " + str(lng)) - - - # It did happen that the generation of photos crashed as, for some reason, - # a file in _cache was apparently used by another process (hint: it was - # not). I therefore need this test before deleting a file in _cache, so - # that generation of the orthos can move forward. - def _isFileAccessibleWin(self, src): - a = False - if os.path.isfile(src) == True: - try: - os.rename(src, src) - a = True - except OSError as e: - a = False - return a - - # Need a same call for POSIX - def _isFileAccessiblePosix(self, src): - wildcard = "/proc/*/fd/*" - lfds = glob.glob(wildcard) - for fds in lfds: - try: - file = os.readlink(fds) - if file == src: - return True - except OSError as err: - if err.errno == 2: - file = None - else: - raise(err) - return False - - - # This will determine the vertical stepping in degrees in order to generate - # masks with a 1:1 square ratio. This is important as X-Plane textures for - # orthos can only be a power of 2, such as 2048x2048 - def _findVerticalStepping(self): - scale = 1 / math.cos(math.radians(self._lat)) - maxlat = (1 / scale) * mstr_zl_18 - return maxlat - - - # To write down X-Plane .ter files, we will need to know the exact size - # of the particular longitude we are in, as this value varies depending - # on where you are on a sphere. - # Returned values is in meters. - # The current latitude is needed. - def _findWidthOfLongitude(self, lat): - dm = math.cos(math.radians(lat)) * 111.321 # <- 1 deg width at equator in km - return round(dm * 1000, 3) - - - # Builds and processes the tile with everything required, in one call. - def _buildTile(self): - mstr_msg("orthographic", "Beginning construction of tile") - - # We need to know which platform we are on - os_platform = os.name - - # Create the _cache folder, should it not exist. - # Temporary images for the ortho tile generation go here - if not os.path.exists(self._output + "/_cache"): - os.makedirs(self._output + "/_cache") - mstr_msg("orthographic", "Created _cache folder.") - - # Generate the Tiles folder for the finished products - if not os.path.exists(self._output + "/Tiles"): - os.makedirs(self._output + "/Tiles") - mstr_msg("orthographic", "Created Tiles folder.") - - # Generate the Tiles/lat-lng folder for the finished tile - if not os.path.exists(self._output + "/Tiles/z_orthographic_" + self._latlngfld): - os.makedirs(self._output + "/Tiles/z_orthographic_"+ self._latlngfld) - mstr_msg("orthographic", "Created Tiles sub folder: " + self._latlngfld) - - # Generate the orthos folder - if not os.path.exists(self._output + "/Tiles/z_orthographic_" + self._latlngfld + "/orthos"): - os.makedirs(self._output + "/Tiles/z_orthographic_" + self._latlngfld +"/orthos") - mstr_msg("orthographic", "Created tile orthos folder") - - if mstr_xp_genscenery == True: - if not os.path.exists(self._output + "/Tiles/z_orthographic_" + self._latlngfld + "/terrain"): - os.makedirs(self._output + "/Tiles/z_orthographic_" + self._latlngfld + "/terrain") - mstr_msg("orthographic", "Created X-Plane tile terrain folder") - - if mstr_xp_generate_normal_maps == True: - if not os.path.exists(self._output + "/Tiles/z_orthographic_" + self._latlngfld + "/normals"): - os.makedirs(self._output + "/Tiles/z_orthographic_" + self._latlngfld + "/normals") - mstr_msg("orthographic", "Created X-Plane tile normals folder") - - # The tile is constructed of many smaller parts. We walk through the - # smallest possible, from which the bigger ones are later built. - bb_lat = self._lat - bb_lng = self._long - bb_lat_edge = self._lat+self._vstep - bb_lng_edge = self._long+mstr_zl_18 - cur_tile_x = 1 - cur_tile_y = 1 - osmxml = mstr_osmxml(0,0) - mstr_msg("orthographic", "Set initial coordinates and bounding box for OSM acquisition") - - # The highest encountered tile numbers - # This is needed to produce the zoom level 16 images - top_lat = 1 - top_lng = 1 - - # We need to know the highest possible latitude and longitude tile numbers, - # in case we render at the edge - mlat = 1 - mlng = 1 - while bb_lat < self._lat + 1: - bb_lat = bb_lat + self._vstep - mlat = mlat+1 - while bb_lng < self._long + 1: - bb_lng = bb_lng + mstr_zl_18 - mlng = mlng+1 - mstr_msg("orthographic", "Max lat tile: " + str(mlat) + " - max lng tile: " + str(mlng)) - maxlatlng = [ mlat, mlng ] - - # Reset these two - bb_lat = self._lat - bb_lng = self._long - - # For X-Plane scenery generation - xp_datagroup = 1 - - # Previously, I downloaded all XML files in one go - but to ease the - # stress on OSM servers and my server, we will do acquire the data - # only for the current processed part of the tile. - while bb_lat < self._lat + 1: - while bb_lng < self._long + 1: - # Adjust bounding box - osmxml.adjust_bbox(bb_lat, bb_lng, bb_lat_edge, bb_lng_edge) - mstr_msg("orthographic", "Adjusted bounding box for XML object") - - # Determine what to do... maybe work was interrupted - if os.path.isfile(mstr_datafolder + "Tiles/" + self._latlngfld + "/orthos/" + str(cur_tile_y) + "_" + str(cur_tile_x) + ".dds") == False: - - # Let the user know - mstr_msg("orthographic", "Generating missing orthophoto " + str(cur_tile_y) + "-" + str(cur_tile_x)) - - # Get the data - osmxml.acquire_osm(cur_tile_y, cur_tile_x) # <- This acquires current OSM info - mstr_msg("orthographic", "Acquired current OSM info from marstr.online repository") - - # Check for work to be done - layers = self.determineLayerWork() - - # We need to walk through the array of layers, - # in their z-order. - # For each layer, we will generate the mask, the layer image - # itself, and finally, compose the ortho photo. - mstr_msg("orthographic", "Beginning generation of layers") - - curlyr = 1 - for layer in layers: - # Let the user know - mstr_msg("orthographic", "Processing layer " + str(curlyr) + " of " + str(len(layers))) - - # Generate the mask - mg = mstr_maskgen( [self._lat, cur_tile_y, self._long, cur_tile_x], self._vstep, layer[0], layer[1], layer[2]) - if mstr_xp_genscenery == True: - mg._set_xpscenery_datagroup(xp_datagroup) - mg._build_mask() - - # Generate the layer - lg = mstr_layergen(layer[0], layer[1], self._lat, cur_tile_y, self._long, cur_tile_x, layer[2]) - lg.set_max_latlng_tile(maxlatlng) - lg.set_latlng_folder(self._latlngfld) - lg.open_db() - lg.genlayer() - curlyr = curlyr+1 - mstr_msg("orthographic", "All layers created") - - # We should have all layers now. - # Snap a photo with our satellite :) - mstr_msg("orthographic", "Generating ortho photo") - pg = mstr_photogen(self._lat, self._long, cur_tile_y, cur_tile_x, maxlatlng[0], maxlatlng[1]) - pg.genphoto() - mstr_msg("orthographic", " -- Ortho photo generated -- ") - print("") - print("") - - # Adjust longitude coordinates - cur_tile_x = cur_tile_x+1 - bb_lng = bb_lng + mstr_zl_18 - bb_lng_edge = bb_lng_edge + mstr_zl_18 - mstr_msg("orthographic", "Adjustment of longitude performed") - # Adjust peak longitude tile number - if cur_tile_x > top_lng: - top_lng = cur_tile_x - - # Clear out cache - if mstr_clear_cache == True: - ch = glob.glob(mstr_datafolder + "_cache/*") - for f in ch: - if os_platform == "nt": - if self._isFileAccessibleWin(f) == True: - os.remove(f) - if os_platform == "posix": - if self._isFileAccessiblePosix(f) == True: - os.remove(f) - mstr_msg("orthographic", "Cleared cache") - - - # Adjust latitude and all other values when we get here - cur_tile_y = cur_tile_y+1 - cur_tile_x = 1 - bb_lng = self._long - bb_lng_edge = self._long + mstr_zl_18 - bb_lat = bb_lat + self._vstep - bb_lat_edge = bb_lat_edge + self._vstep - mstr_msg("orthographic", "Adjustment of latitude performed") - # Adjust peak latitude number - if cur_tile_y > top_lat: - top_lat = cur_tile_y - - mstr_msg("orthographic", "Generation of all tiles completed!") - - - # Complete scenery - if mstr_xp_genscenery == True: - dsf = mstr_xp_dsfgen(self._lat, self._long, mlat, mlng, self._vstep) - dsf.build_dsf_for_tile() - mstr_msg("orthographic", "X-Plane scenery completed") - - mstr_msg("orthographic", "Final step completed.") - mstr_msg("orthographic", "Tile data in: " + self._output + "/Tiles/z_orthographic_" + self._latlngfld) - print("") - mstr_msg("orthographic", "Thanks for using Orthographic! -- Best, Marcus") - print("") - - # Let's leave this out for the moment - """ - mstr_msg("orthographic", "Generating ZL16 tiles and keeping airport tiles") - tg = mstr_tilegen(self._lat, self._lng, self._vstep, top_lat, top_lng) - tg.genTiles() - mstr_msg("orthographic", "Final step completed.") - print("") - mstr_msg("orthographic", "Tile data in: " + mstr_datafolder + "/Tiles/" + str(self._lat) + "_" + self._lng) - print("") - print("") - mstr_msg("orthographic", "Thanks for using Orthographic! -- Best, Marcus") - print("") - """ - - - - - - # Checks which layers need to be generated, and what kind of layer it is - def determineLayerWork(self): - - mstr_msg("orthographic", "Checking for work to be performed") - - layers = [] - - tilexml = mstr_datafolder + "_cache/tile.xml" - xml = mstr_osmxml(0,0) - way = xml.acquire_waypoint_data(tilexml) - rls = xml.acquire_relations(tilexml) - - for l in mstr_ortho_layers: - # Check if there is anything to render - has_way = False - has_rls = False - for w in way: - if w[2] == l[0] and w[3] == l[1]: - has_way = True - break - for r in rls: - if l[0] in r[1] and l[1] in r[1]: - has_rls = True - break - - if has_way == True or has_rls == True: - mstr_msg("orthographic", "Adding: " + l[0]+":"+l[1]) - is_line = False - for s in mstr_ortho_layers: - if s[0] == l[0] and s[1] == l[1]: - if isinstance(s[2], int) == False: - is_line = False - break - if isinstance(s[2], int) == True: - is_line = True - break - ly = (l[0], l[1], is_line) - layers.append(ly) - - mstr_msg("orthographic", "A total of " + str(len(layers)) + " layers were found") - return layers - - - # Construct a folder name for latitude and longitude - def latlng_folder(self, numbers): - fstr = "" - if numbers[0] >= 0: fstr = "+" - if numbers[0] < 0: fstr = "-" - if abs(numbers[0]) < 10: fstr = fstr + "0" + str(numbers[0]) - if abs(numbers[0]) >= 10 and numbers[0] <= 90: fstr = fstr + str(numbers[0]) - - if numbers[1] >= 0: fstr = fstr + "+" - if numbers[1] < 0: fstr = fstr + "-" - if abs(numbers[1]) < 10: fstr = fstr + "00" + str(numbers[1]) - if abs(numbers[1]) >= 10 and numbers[0] <= 99: fstr = fstr + "0" + str(numbers[1]) - if abs(numbers[1]) >= 100 : fstr = fstr + str(numbers[1]) - - return fstr - + +# ------------------------------------------------------------------- +# ORTHOGRAPHIC +# Your personal aerial satellite. Always on. At any altitude.* +# Developed by MarStrMind +# License: Open Software License 3.0 +# Up to date version always on marstr.online +# ------------------------------------------------------------------- +# orthographic.py +# Main class which handles the generation of the ortho tile. +# ------------------------------------------------------------------- + +import math +import os +import glob +from defines import * +from log import * +from maskgen import * +from layergen import * +from photogen import * +from osmxml import * +from tilegen import * +from xp_dsfgen import * + + +# The main class which handles the rest +class mstr_orthographic: + + # Constructor of class. Takes longitude and latitude. + def __init__(self, lat, lng, outfolder, pwd): + self._lat = lat + self._long = lng + self._output = outfolder + self._pwd = pwd + self._vstep = self._findVerticalStepping() + self._latlngfld = self.latlng_folder([lat,lng]) + mstr_msg("orthographic", "Initiated with LAT: " + str(lat) + ", LNG: " + str(lng)) + + + # It did happen that the generation of photos crashed as, for some reason, + # a file in _cache was apparently used by another process (hint: it was + # not). I therefore need this test before deleting a file in _cache, so + # that generation of the orthos can move forward. + def _isFileAccessibleWin(self, src): + a = False + if os.path.isfile(src) == True: + try: + os.rename(src, src) + a = True + except OSError as e: + a = False + return a + + # Need a same call for POSIX + def _isFileAccessiblePosix(self, src): + wildcard = "/proc/*/fd/*" + lfds = glob.glob(wildcard) + for fds in lfds: + try: + file = os.readlink(fds) + if file == src: + return True + except OSError as err: + if err.errno == 2: + file = None + else: + raise(err) + return False + + + # This will determine the vertical stepping in degrees in order to generate + # masks with a 1:1 square ratio. This is important as X-Plane textures for + # orthos can only be a power of 2, such as 2048x2048 + def _findVerticalStepping(self): + scale = 1 / math.cos(math.radians(self._lat)) + maxlat = (1 / scale) * mstr_zl_18 + return maxlat + + + # To write down X-Plane .ter files, we will need to know the exact size + # of the particular longitude we are in, as this value varies depending + # on where you are on a sphere. + # Returned values is in meters. + # The current latitude is needed. + def _findWidthOfLongitude(self, lat): + dm = math.cos(math.radians(lat)) * 111.321 # <- 1 deg width at equator in km + return round(dm * 1000, 3) + + + # Builds and processes the tile with everything required, in one call. + def _buildTile(self): + mstr_msg("orthographic", "Beginning construction of tile") + + # We need to know which platform we are on + os_platform = os.name + + # Create the _cache folder, should it not exist. + # Temporary images for the ortho tile generation go here + if not os.path.exists(self._output + "/_cache"): + os.makedirs(self._output + "/_cache") + mstr_msg("orthographic", "Created _cache folder.") + + # Generate the Tiles folder for the finished products + if not os.path.exists(self._output + "/Tiles"): + os.makedirs(self._output + "/Tiles") + mstr_msg("orthographic", "Created Tiles folder.") + + # Generate the Tiles/lat-lng folder for the finished tile + if not os.path.exists(self._output + "/Tiles/z_orthographic_" + self._latlngfld): + os.makedirs(self._output + "/Tiles/z_orthographic_"+ self._latlngfld) + mstr_msg("orthographic", "Created Tiles sub folder: " + self._latlngfld) + + # Generate the orthos folder + if not os.path.exists(self._output + "/Tiles/z_orthographic_" + self._latlngfld + "/orthos"): + os.makedirs(self._output + "/Tiles/z_orthographic_" + self._latlngfld +"/orthos") + mstr_msg("orthographic", "Created tile orthos folder") + + if mstr_xp_genscenery == True: + if not os.path.exists(self._output + "/Tiles/z_orthographic_" + self._latlngfld + "/terrain"): + os.makedirs(self._output + "/Tiles/z_orthographic_" + self._latlngfld + "/terrain") + mstr_msg("orthographic", "Created X-Plane tile terrain folder") + + if mstr_xp_generate_normal_maps == True: + if not os.path.exists(self._output + "/Tiles/z_orthographic_" + self._latlngfld + "/normals"): + os.makedirs(self._output + "/Tiles/z_orthographic_" + self._latlngfld + "/normals") + mstr_msg("orthographic", "Created X-Plane tile normals folder") + + # The tile is constructed of many smaller parts. We walk through the + # smallest possible, from which the bigger ones are later built. + bb_lat = self._lat + bb_lng = self._long + bb_lat_edge = self._lat+self._vstep + bb_lng_edge = self._long+mstr_zl_18 + cur_tile_x = 1 + cur_tile_y = 1 + osmxml = mstr_osmxml(0,0) + mstr_msg("orthographic", "Set initial coordinates and bounding box for OSM acquisition") + + # The highest encountered tile numbers + # This is needed to produce the zoom level 16 images + top_lat = 1 + top_lng = 1 + + # We need to know the highest possible latitude and longitude tile numbers, + # in case we render at the edge + mlat = 1 + mlng = 1 + while bb_lat < self._lat + 1: + bb_lat = bb_lat + self._vstep + mlat = mlat+1 + while bb_lng < self._long + 1: + bb_lng = bb_lng + mstr_zl_18 + mlng = mlng+1 + mstr_msg("orthographic", "Max lat tile: " + str(mlat) + " - max lng tile: " + str(mlng)) + maxlatlng = [ mlat, mlng ] + + # Reset these two + bb_lat = self._lat + bb_lng = self._long + + # For X-Plane scenery generation + xp_datagroup = 1 + + # Previously, I downloaded all XML files in one go - but to ease the + # stress on OSM servers and my server, we will do acquire the data + # only for the current processed part of the tile. + while bb_lat < self._lat + 1: + while bb_lng < self._long + 1: + # Adjust bounding box + osmxml.adjust_bbox(bb_lat, bb_lng, bb_lat_edge, bb_lng_edge) + mstr_msg("orthographic", "Adjusted bounding box for XML object") + + # Determine what to do... maybe work was interrupted + if os.path.isfile(mstr_datafolder + "Tiles/" + self._latlngfld + "/orthos/" + str(cur_tile_y) + "_" + str(cur_tile_x) + ".dds") == False: + + # Let the user know + mstr_msg("orthographic", "Generating missing orthophoto " + str(cur_tile_y) + "-" + str(cur_tile_x)) + + # Get the data + osmxml.acquire_osm(cur_tile_y, cur_tile_x) # <- This acquires current OSM info + mstr_msg("orthographic", "Acquired current OSM info from marstr.online repository") + + # Check for work to be done + layers = self.determineLayerWork() + + # We need to walk through the array of layers, + # in their z-order. + # For each layer, we will generate the mask, the layer image + # itself, and finally, compose the ortho photo. + mstr_msg("orthographic", "Beginning generation of layers") + + curlyr = 1 + for layer in layers: + # Let the user know + mstr_msg("orthographic", "Processing layer " + str(curlyr) + " of " + str(len(layers))) + + # Generate the mask + mg = mstr_maskgen( [self._lat, cur_tile_y, self._long, cur_tile_x], self._vstep, layer[0], layer[1], layer[2]) + if mstr_xp_genscenery == True: + mg._set_xpscenery_datagroup(xp_datagroup) + mg._build_mask() + + # Generate the layer + lg = mstr_layergen(layer[0], layer[1], self._lat, cur_tile_y, self._long, cur_tile_x, layer[2]) + lg.set_max_latlng_tile(maxlatlng) + lg.set_latlng_folder(self._latlngfld) + lg.open_db() + lg.genlayer() + curlyr = curlyr+1 + mstr_msg("orthographic", "All layers created") + + # We should have all layers now. + # Snap a photo with our satellite :) + mstr_msg("orthographic", "Generating ortho photo") + pg = mstr_photogen(self._lat, self._long, cur_tile_y, cur_tile_x, maxlatlng[0], maxlatlng[1]) + pg.genphoto() + mstr_msg("orthographic", " -- Ortho photo generated -- ") + print("") + print("") + + # Adjust longitude coordinates + cur_tile_x = cur_tile_x+1 + bb_lng = bb_lng + mstr_zl_18 + bb_lng_edge = bb_lng_edge + mstr_zl_18 + mstr_msg("orthographic", "Adjustment of longitude performed") + # Adjust peak longitude tile number + if cur_tile_x > top_lng: + top_lng = cur_tile_x + + # Clear out cache + if mstr_clear_cache == True: + ch = glob.glob(mstr_datafolder + "_cache/*") + for f in ch: + if os_platform == "nt": + if self._isFileAccessibleWin(f) == True: + os.remove(f) + if os_platform == "posix": + if self._isFileAccessiblePosix(f) == True: + os.remove(f) + mstr_msg("orthographic", "Cleared cache") + + + # Adjust latitude and all other values when we get here + cur_tile_y = cur_tile_y+1 + cur_tile_x = 1 + bb_lng = self._long + bb_lng_edge = self._long + mstr_zl_18 + bb_lat = bb_lat + self._vstep + bb_lat_edge = bb_lat_edge + self._vstep + mstr_msg("orthographic", "Adjustment of latitude performed") + # Adjust peak latitude number + if cur_tile_y > top_lat: + top_lat = cur_tile_y + + mstr_msg("orthographic", "Generation of all tiles completed!") + + + # Complete scenery + if mstr_xp_genscenery == True: + dsf = mstr_xp_dsfgen(self._lat, self._long, mlat, mlng, self._vstep) + dsf.build_dsf_for_tile() + mstr_msg("orthographic", "X-Plane scenery completed") + + mstr_msg("orthographic", "Final step completed.") + mstr_msg("orthographic", "Tile data in: " + self._output + "/Tiles/z_orthographic_" + self._latlngfld) + print("") + mstr_msg("orthographic", "Thanks for using Orthographic! -- Best, Marcus") + print("") + + # Let's leave this out for the moment + """ + mstr_msg("orthographic", "Generating ZL16 tiles and keeping airport tiles") + tg = mstr_tilegen(self._lat, self._lng, self._vstep, top_lat, top_lng) + tg.genTiles() + mstr_msg("orthographic", "Final step completed.") + print("") + mstr_msg("orthographic", "Tile data in: " + mstr_datafolder + "/Tiles/" + str(self._lat) + "_" + self._lng) + print("") + print("") + mstr_msg("orthographic", "Thanks for using Orthographic! -- Best, Marcus") + print("") + """ + + + + + + # Checks which layers need to be generated, and what kind of layer it is + def determineLayerWork(self): + + mstr_msg("orthographic", "Checking for work to be performed") + + layers = [] + + tilexml = mstr_datafolder + "_cache/tile.xml" + xml = mstr_osmxml(0,0) + way = xml.acquire_waypoint_data(tilexml) + rls = xml.acquire_relations(tilexml) + + for l in mstr_ortho_layers: + # Check if there is anything to render + has_way = False + has_rls = False + for w in way: + if w[2] == l[0] and w[3] == l[1]: + has_way = True + break + for r in rls: + if l[0] in r[1] and l[1] in r[1]: + has_rls = True + break + + if has_way == True or has_rls == True: + mstr_msg("orthographic", "Adding: " + l[0]+":"+l[1]) + is_line = False + for s in mstr_ortho_layers: + if s[0] == l[0] and s[1] == l[1]: + if isinstance(s[2], int) == False: + is_line = False + break + if isinstance(s[2], int) == True: + is_line = True + break + ly = (l[0], l[1], is_line) + layers.append(ly) + + mstr_msg("orthographic", "A total of " + str(len(layers)) + " layers were found") + return layers + + + # Construct a folder name for latitude and longitude + def latlng_folder(self, numbers): + fstr = "" + if numbers[0] >= 0: fstr = "+" + if numbers[0] < 0: fstr = "-" + if abs(numbers[0]) < 10: fstr = fstr + "0" + str(numbers[0]) + if abs(numbers[0]) >= 10 and numbers[0] <= 90: fstr = fstr + str(numbers[0]) + + if numbers[1] >= 0: fstr = fstr + "+" + if numbers[1] < 0: fstr = fstr + "-" + if abs(numbers[1]) < 10: fstr = fstr + "00" + str(numbers[1]) + if abs(numbers[1]) >= 10 and numbers[0] <= 99: fstr = fstr + "0" + str(numbers[1]) + if abs(numbers[1]) >= 100 : fstr = fstr + str(numbers[1]) + + return fstr + diff --git a/osmxml.py b/osmxml.py index faf529a..b734334 100644 --- a/osmxml.py +++ b/osmxml.py @@ -1,172 +1,172 @@ - -# ------------------------------------------------------------------- -# ORTHOGRAPHIC -# Your personal aerial satellite. Always on. At any altitude.* -# Developed by MarStrMind -# License: Open Software License 3.0 -# Up to date version always on marstr.online -# ------------------------------------------------------------------- -# osmxml.py -# Performs calls to Overpass API to acquire XML files which we can -# then store into a much faster SQLite3 database. -# ------------------------------------------------------------------- - - -import xml.dom.minidom -import requests -import os -from defines import * -from log import * - -class mstr_osmxml: - def __init__(self, lat, lng): - self._latv = lat - self._lngv = lng - self._lat = lat - self._lng = lng - self._curB_lat = lat + mstr_zl_18 - self._curB_lng = lng + mstr_zl_18 - - - # Adjust bbox for when this class should persost, but acquire data for a different bbox - def adjust_bbox(self, lat, lng, lat_e, lng_e): - self._lat = round(lat, 4) - self._lng = round(lng, 4) - self._curB_lat = round(lat_e, 4) - self._curB_lng = round(lng_e, 4) - - - # Acquire XMLs in chunks, then store them - def acquire_osm(self, v, h, asobject=False): - mstr_msg("osmxml", "Acquiring OSM data for " + str(self._lat)+","+str(self._lng)+" - "+str(self._curB_lat)+","+str(self._curB_lng)) - - # We will use our self-hosted API for this. - data = { - "bbox": { - "lat": str(self._lat), - "lng": str(self._lng), - "lat_b": str(self._curB_lat), - "lng_b": str(self._curB_lng) - }, - "tile_lat": str(self._lat), - "tile_lng": str(self._lng), - "square_lat": str(v), - "square_lng": str(h) - } - r = requests.post(mstr_osm_endpoint, json=data) - - xmlf = mstr_datafolder + "_cache/tile.xml" - if os.path.isfile(xmlf): - os.remove(xmlf) - with open(xmlf, 'wb') as textfile: - textfile.write(r.content) - - # Provide the object directly - if asobject == True: - xml_doc = xml.dom.minidom.parse("_cache/tile.xml") - return xml_doc - - - # Get all nodes from the specified OSM file - def acquire_nodes(self, xmlfile): - xml_doc = xml.dom.minidom.parse(xmlfile) - nodedata = xml_doc.getElementsByTagName("node") - nodes = [] - for node in nodedata: - p = (node.getAttribute("id"), node.getAttribute("lat"), node.getAttribute("lon")) - nodes.append(p) - return nodes - - - # Get all waypoint data - def acquire_waypoint_data(self, xmlfile): - xml_doc = xml.dom.minidom.parse(xmlfile) - wpdata = xml_doc.getElementsByTagName("way") - wps = [] - for wp in wpdata: - nddata = wp.getElementsByTagName("nd") - for nd in nddata: - p = (wp.getAttribute("id"), nd.getAttribute("ref"), "", "") - wps.append(p) - for wp in wpdata: - tagdata = wp.getElementsByTagName("tag") - for tag in tagdata: - c = tag.getAttribute("v").replace(",", "") - c = c.replace("'", "") - p = (wp.getAttribute("id"), "NULL", tag.getAttribute("k"), c) - wps.append(p) - return wps - - - # Checks if there is an airport or many airports with an ICAO code in the - # supplied XML data chunk - def find_icao_codes(self, xmlfile): - icao = [] - xml_doc = xml.dom.minidom.parse(xmlfile) - wpdata = xml_doc.getElementsByTagName("way") - for wp in wpdata: - tags = wp.getElementsByTagName("tag") - for tag in tags: - a = tag.getAttribute("k") - if a == "icao": - v = tag.getAttribute("v") - icao.append(v) - # Return list of found airports - return icao - - - # Finds the surface type of a runway in the current data chunk. - # If no surface type is specified, the runway will be rendered similar - # to how motorways are rendered. - # This gets called only if some ICAO code was found - def find_runway_surface(self, xmlfile): - surface = "" - wpid = "" - xml_doc = xml.dom.minidom.parse(xmlfile) - wpdata = xml_doc.getElementsByTagName("way") - for wp in wpdata: - tags = wp.getElementsByTagName("tag") - for tag in tags: - a = tag.getAttribute("k") - v = tag.getAttribute("v") - if a == "aeroway" and v == "runway": - wpid = wp.getAttribute("id") - break - for wp in wpdata: - wid = wp.getAttribute("id") - if wid == wpid: - tags = wp.getElementsByTagName("tag") - for tag in tags: - a = tag.getAttribute("k") - v = tag.getAttribute("v") - if a == "surface": - surface = v - - # Return the found surface type - return surface - - - # It turns out that some features hide themselves in the relations section. - # I figured this out during testing, and almost going insane over the - # question as to why some parts like forests are missing in the masks, while - # it is clearly labeled as forest on openstreetmap. We need to scan - # the relations and then scan through the outer and inner way IDs. - # - # Get all relation entries of importance. - def acquire_relations(self, xmlfile): - xml_doc = xml.dom.minidom.parse(xmlfile) - rlxml = xml_doc.getElementsByTagName("relation") - rls = [] - for rl in rlxml: - rldata = rl.getElementsByTagName("member") - tagdata = rl.getElementsByTagName("tag") - for rp in rldata: - t = rp.getAttribute("role") - if t == "inner" or t == "outer": - tgd = [] - for i in tagdata: - tgd.append(i.getAttribute("k")) - tgd.append(i.getAttribute("v")) - rls.append((rp.getAttribute("ref"), tgd)) - return rls + +# ------------------------------------------------------------------- +# ORTHOGRAPHIC +# Your personal aerial satellite. Always on. At any altitude.* +# Developed by MarStrMind +# License: Open Software License 3.0 +# Up to date version always on marstr.online +# ------------------------------------------------------------------- +# osmxml.py +# Performs calls to Overpass API to acquire XML files which we can +# then store into a much faster SQLite3 database. +# ------------------------------------------------------------------- + + +import xml.dom.minidom +import requests +import os +from defines import * +from log import * + +class mstr_osmxml: + def __init__(self, lat, lng): + self._latv = lat + self._lngv = lng + self._lat = lat + self._lng = lng + self._curB_lat = lat + mstr_zl_18 + self._curB_lng = lng + mstr_zl_18 + + + # Adjust bbox for when this class should persost, but acquire data for a different bbox + def adjust_bbox(self, lat, lng, lat_e, lng_e): + self._lat = round(lat, 4) + self._lng = round(lng, 4) + self._curB_lat = round(lat_e, 4) + self._curB_lng = round(lng_e, 4) + + + # Acquire XMLs in chunks, then store them + def acquire_osm(self, v, h, asobject=False): + mstr_msg("osmxml", "Acquiring OSM data for " + str(self._lat)+","+str(self._lng)+" - "+str(self._curB_lat)+","+str(self._curB_lng)) + + # We will use our self-hosted API for this. + data = { + "bbox": { + "lat": str(self._lat), + "lng": str(self._lng), + "lat_b": str(self._curB_lat), + "lng_b": str(self._curB_lng) + }, + "tile_lat": str(self._lat), + "tile_lng": str(self._lng), + "square_lat": str(v), + "square_lng": str(h) + } + r = requests.post(mstr_osm_endpoint, json=data) + + xmlf = mstr_datafolder + "_cache/tile.xml" + if os.path.isfile(xmlf): + os.remove(xmlf) + with open(xmlf, 'wb') as textfile: + textfile.write(r.content) + + # Provide the object directly + if asobject == True: + xml_doc = xml.dom.minidom.parse("_cache/tile.xml") + return xml_doc + + + # Get all nodes from the specified OSM file + def acquire_nodes(self, xmlfile): + xml_doc = xml.dom.minidom.parse(xmlfile) + nodedata = xml_doc.getElementsByTagName("node") + nodes = [] + for node in nodedata: + p = (node.getAttribute("id"), node.getAttribute("lat"), node.getAttribute("lon")) + nodes.append(p) + return nodes + + + # Get all waypoint data + def acquire_waypoint_data(self, xmlfile): + xml_doc = xml.dom.minidom.parse(xmlfile) + wpdata = xml_doc.getElementsByTagName("way") + wps = [] + for wp in wpdata: + nddata = wp.getElementsByTagName("nd") + for nd in nddata: + p = (wp.getAttribute("id"), nd.getAttribute("ref"), "", "") + wps.append(p) + for wp in wpdata: + tagdata = wp.getElementsByTagName("tag") + for tag in tagdata: + c = tag.getAttribute("v").replace(",", "") + c = c.replace("'", "") + p = (wp.getAttribute("id"), "NULL", tag.getAttribute("k"), c) + wps.append(p) + return wps + + + # Checks if there is an airport or many airports with an ICAO code in the + # supplied XML data chunk + def find_icao_codes(self, xmlfile): + icao = [] + xml_doc = xml.dom.minidom.parse(xmlfile) + wpdata = xml_doc.getElementsByTagName("way") + for wp in wpdata: + tags = wp.getElementsByTagName("tag") + for tag in tags: + a = tag.getAttribute("k") + if a == "icao": + v = tag.getAttribute("v") + icao.append(v) + # Return list of found airports + return icao + + + # Finds the surface type of a runway in the current data chunk. + # If no surface type is specified, the runway will be rendered similar + # to how motorways are rendered. + # This gets called only if some ICAO code was found + def find_runway_surface(self, xmlfile): + surface = "" + wpid = "" + xml_doc = xml.dom.minidom.parse(xmlfile) + wpdata = xml_doc.getElementsByTagName("way") + for wp in wpdata: + tags = wp.getElementsByTagName("tag") + for tag in tags: + a = tag.getAttribute("k") + v = tag.getAttribute("v") + if a == "aeroway" and v == "runway": + wpid = wp.getAttribute("id") + break + for wp in wpdata: + wid = wp.getAttribute("id") + if wid == wpid: + tags = wp.getElementsByTagName("tag") + for tag in tags: + a = tag.getAttribute("k") + v = tag.getAttribute("v") + if a == "surface": + surface = v + + # Return the found surface type + return surface + + + # It turns out that some features hide themselves in the relations section. + # I figured this out during testing, and almost going insane over the + # question as to why some parts like forests are missing in the masks, while + # it is clearly labeled as forest on openstreetmap. We need to scan + # the relations and then scan through the outer and inner way IDs. + # + # Get all relation entries of importance. + def acquire_relations(self, xmlfile): + xml_doc = xml.dom.minidom.parse(xmlfile) + rlxml = xml_doc.getElementsByTagName("relation") + rls = [] + for rl in rlxml: + rldata = rl.getElementsByTagName("member") + tagdata = rl.getElementsByTagName("tag") + for rp in rldata: + t = rp.getAttribute("role") + if t == "inner" or t == "outer": + tgd = [] + for i in tagdata: + tgd.append(i.getAttribute("k")) + tgd.append(i.getAttribute("v")) + rls.append((rp.getAttribute("ref"), tgd)) + return rls \ No newline at end of file diff --git a/photogen.py b/photogen.py index 51c9778..5703755 100644 --- a/photogen.py +++ b/photogen.py @@ -1,192 +1,192 @@ - -import os -from PIL import Image, ImageFilter -from defines import * -from layergen import * -from log import * -from wand import image - -# ------------------------------------------------------------------- -# ORTHOGRAPHIC -# Your personal aerial satellite. Always on. At any altitude.* -# Developed by MarStrMind -# License: Open Software License 3.0 -# Up to date version always on marstr.online -# ------------------------------------------------------------------- -# photogen.py -# The class that generates the photo tiles from previous layers, -# in their correct order. -# ------------------------------------------------------------------- - - -class mstr_photogen: - - # Initializer doesn't need much - def __init__ (self, lat, lng, ty, tx, maxlat, maxlng): - self._lat = lat - self._lng = lng - self._ty = ty - self._tx = tx - self._maxlatlng = [ maxlat, maxlng ] - # Define layer size depending on what is wanted - self._imgsize = 0 - if mstr_photores == 2048: self._imgsize = 3000 - if mstr_photores == 4096: self._imgsize = 6000 - # Empty image where everything goes into - self._tile = Image.new("RGBA", (self._imgsize, self._imgsize)) - self._latlngfld = self.latlng_folder([lat,lng]) - mstr_msg("photogen", "Photogen initialized") - - - # This puts it all together. Bonus: AND saves it. - def genphoto(self): - # Template for the file name which is always the same - root_filename = mstr_datafolder + "/_cache/" + str(self._lat) + "-" + str(self._ty) + "_" + str(self._lng) + "-" + str(self._tx) + "_" - - # First, we walk through all layers and blend them on top of each other, in order - mstr_msg("photogen", "Merging layers") - - for l in mstr_ortho_layers: - if os.path.isfile(root_filename + l[0] + "-" + l[1] + "_layer.png"): - # Need to divert in case we have shadows - if mstr_shadow_enabled == True: - if os.path.isfile(root_filename + l[0] + "-" + l[1] + "_layer_shadow.png"): - sn = root_filename + l[0] + "-" + l[1] + "_layer_shadow.png" - s_layer = Image.open(sn) - self._tile.alpha_composite(s_layer) - # Complete the file name based on the template - fn = root_filename + l[0] + "-" + l[1] + "_layer.png" - # Open the layer - layer = Image.open(fn) - # Converge the layer with this image - self._tile.alpha_composite(layer) - - # When we have run through this loop, we will end up with a sandwiched - # image of all the other images, in their correct order. - # However, since I have discovered that some areas in OSM simply do not - # have any tag or information, it is possible that the final image will - # have empty, alpha-transparent patches. - # For this reason we need to check against these and fix that. - - # First, we will check if there is something to fix: - emptyspace = self.checkForEmptySpace() - mstr_msg("photogen", "Checked for empty patches") - - # If this check comes back as true, we need to perform - # aforementioned fix: - if emptyspace == True: - # Choose a suitable layer type - lt = [6,14,17] - pick = randrange(0,len(lt)-1) - ltp = lt[pick] - tag = mstr_ortho_layers[ltp][0] - value = mstr_ortho_layers[ltp][1] - - mstr_msg("photogen", "Patching empty space") - self.buildCompletionMask() - - # Generate the layer as if it were part of the OSM data - lg = mstr_layergen(tag, value, self._lat, self._ty, self._lng, self._tx, False, is_completion=True) - lg.set_max_latlng_tile(self._maxlatlng) - lg.set_latlng_folder(self._latlngfld) - lg.open_db() - lg.genlayer() - - # Load the image - completion = Image.open(root_filename + "tile-completion_layer.png") - - # Merge the images - completion.alpha_composite(self._tile) - - # Make this the real one - self._tile = completion - - - # There may be some tiles that have a larger sea or even an ocean in them - these need to be - # removed from the final tile - ocean_pix = self._tile.load() - for y in range(self._tile.width): - for x in range(self._tile.height): - p = ocean_pix[x,y] - if p[0] == 255 and p[1] == 0 and p[2] == 255: - t = (0,0,0,0) - ocean_pix[x,y] = t - - # We are now in posession of the final image. - - # Scale to correct size. - self._tile = self._tile.resize((mstr_photores, mstr_photores), Image.Resampling.BILINEAR) - - # This we can save accordingly. - self._tile.convert('RGB').save(mstr_datafolder + "Tiles/z_orthographic_" + self._latlngfld + "/orthos/" + str(self._ty) + "_" + str(self._tx) + ".png") - - - # Now we convert this into a DDS - with image.Image(filename=mstr_datafolder + "Tiles/z_orthographic_" + self._latlngfld + "/orthos/" + str(self._ty) + "_" + str(self._tx) + ".png") as img: - img.compression = "dxt1" - img.save(filename=mstr_datafolder + "Tiles/z_orthographic_" + self._latlngfld + "/orthos/" + str(self._ty) + "_" + str(self._tx) + ".dds") - # TODO: CUT OUT OCEANS AND SEAS IN DDS - os.remove(mstr_datafolder + "Tiles/z_orthographic_" + self._latlngfld + "/orthos/" + str(self._ty) + "_" + str(self._tx) + ".png") - - - - - # This checks the final image for empty patches. Should one be - # found, we will generate something to fill the gap. If this is - # the case, we will also note this in the database for the tile, - # under the special tag and value "tile", "completion". The same - # conditions apply for edge testing and so on. - def checkForEmptySpace(self): - empty = False - - # Load photo - layer_pix = self._tile.load() - - # Scan! - for y in range(self._tile.width-1): - for x in range(self._tile.height-1): - p = layer_pix[x,y] - if p[3] < 255: # <- Check for empty or non-complete alpha - empty = True - break - - # Tell about findings - return empty - - - # This returns a mask of the empty space to cover, should there be any - def buildCompletionMask(self): - mask = Image.new("RGBA", (self._imgsize, self._imgsize)) - mask_pix = mask.load() - - # Load photo - layer_pix = self._tile.load() - - # Scan! - for y in range(self._tile.width-1): - for x in range(self._tile.height-1): - p = layer_pix[x,y] - if p[3] < 255: # <- Check for empty or non-complete alpha - mask_pix[x,y] = (0,0,0,255) - # We do not apply any blur or other effects here - we only want the - # exact pixel positions. - - mask.save( mstr_datafolder + "_cache/" + str(self._lat) + "-" + str(self._ty) + "_" + str(self._lng) + "-" + str(self._tx) + "_tile-completion.png" ) - mstr_msg("photogen", "Generated and saved empty space mask") - - - # Construct a folder name for latitude and longitude - def latlng_folder(self, numbers): - fstr = "" - if numbers[0] >= 0: fstr = "+" - if numbers[0] < 0: fstr = "-" - if abs(numbers[0]) < 10: fstr = fstr + "0" + str(numbers[0]) - if abs(numbers[0]) >= 10 and numbers[0] <= 90: fstr = fstr + str(numbers[0]) - - if numbers[1] >= 0: fstr = fstr + "+" - if numbers[1] < 0: fstr = fstr + "-" - if abs(numbers[1]) < 10: fstr = fstr + "00" + str(numbers[1]) - if abs(numbers[1]) >= 10 and numbers[0] <= 99: fstr = fstr + "0" + str(numbers[1]) - if abs(numbers[1]) >= 100 : fstr = fstr + str(numbers[1]) - + +import os +from PIL import Image, ImageFilter +from defines import * +from layergen import * +from log import * +from wand import image + +# ------------------------------------------------------------------- +# ORTHOGRAPHIC +# Your personal aerial satellite. Always on. At any altitude.* +# Developed by MarStrMind +# License: Open Software License 3.0 +# Up to date version always on marstr.online +# ------------------------------------------------------------------- +# photogen.py +# The class that generates the photo tiles from previous layers, +# in their correct order. +# ------------------------------------------------------------------- + + +class mstr_photogen: + + # Initializer doesn't need much + def __init__ (self, lat, lng, ty, tx, maxlat, maxlng): + self._lat = lat + self._lng = lng + self._ty = ty + self._tx = tx + self._maxlatlng = [ maxlat, maxlng ] + # Define layer size depending on what is wanted + self._imgsize = 0 + if mstr_photores == 2048: self._imgsize = 3000 + if mstr_photores == 4096: self._imgsize = 6000 + # Empty image where everything goes into + self._tile = Image.new("RGBA", (self._imgsize, self._imgsize)) + self._latlngfld = self.latlng_folder([lat,lng]) + mstr_msg("photogen", "Photogen initialized") + + + # This puts it all together. Bonus: AND saves it. + def genphoto(self): + # Template for the file name which is always the same + root_filename = mstr_datafolder + "/_cache/" + str(self._lat) + "-" + str(self._ty) + "_" + str(self._lng) + "-" + str(self._tx) + "_" + + # First, we walk through all layers and blend them on top of each other, in order + mstr_msg("photogen", "Merging layers") + + for l in mstr_ortho_layers: + if os.path.isfile(root_filename + l[0] + "-" + l[1] + "_layer.png"): + # Need to divert in case we have shadows + if mstr_shadow_enabled == True: + if os.path.isfile(root_filename + l[0] + "-" + l[1] + "_layer_shadow.png"): + sn = root_filename + l[0] + "-" + l[1] + "_layer_shadow.png" + s_layer = Image.open(sn) + self._tile.alpha_composite(s_layer) + # Complete the file name based on the template + fn = root_filename + l[0] + "-" + l[1] + "_layer.png" + # Open the layer + layer = Image.open(fn) + # Converge the layer with this image + self._tile.alpha_composite(layer) + + # When we have run through this loop, we will end up with a sandwiched + # image of all the other images, in their correct order. + # However, since I have discovered that some areas in OSM simply do not + # have any tag or information, it is possible that the final image will + # have empty, alpha-transparent patches. + # For this reason we need to check against these and fix that. + + # First, we will check if there is something to fix: + emptyspace = self.checkForEmptySpace() + mstr_msg("photogen", "Checked for empty patches") + + # If this check comes back as true, we need to perform + # aforementioned fix: + if emptyspace == True: + # Choose a suitable layer type + lt = [6,14,17] + pick = randrange(0,len(lt)-1) + ltp = lt[pick] + tag = mstr_ortho_layers[ltp][0] + value = mstr_ortho_layers[ltp][1] + + mstr_msg("photogen", "Patching empty space") + self.buildCompletionMask() + + # Generate the layer as if it were part of the OSM data + lg = mstr_layergen(tag, value, self._lat, self._ty, self._lng, self._tx, False, is_completion=True) + lg.set_max_latlng_tile(self._maxlatlng) + lg.set_latlng_folder(self._latlngfld) + lg.open_db() + lg.genlayer() + + # Load the image + completion = Image.open(root_filename + "tile-completion_layer.png") + + # Merge the images + completion.alpha_composite(self._tile) + + # Make this the real one + self._tile = completion + + + # There may be some tiles that have a larger sea or even an ocean in them - these need to be + # removed from the final tile + ocean_pix = self._tile.load() + for y in range(self._tile.width): + for x in range(self._tile.height): + p = ocean_pix[x,y] + if p[0] == 255 and p[1] == 0 and p[2] == 255: + t = (0,0,0,0) + ocean_pix[x,y] = t + + # We are now in posession of the final image. + + # Scale to correct size. + self._tile = self._tile.resize((mstr_photores, mstr_photores), Image.Resampling.BILINEAR) + + # This we can save accordingly. + self._tile.convert('RGB').save(mstr_datafolder + "Tiles/z_orthographic_" + self._latlngfld + "/orthos/" + str(self._ty) + "_" + str(self._tx) + ".png") + + + # Now we convert this into a DDS + with image.Image(filename=mstr_datafolder + "Tiles/z_orthographic_" + self._latlngfld + "/orthos/" + str(self._ty) + "_" + str(self._tx) + ".png") as img: + img.compression = "dxt1" + img.save(filename=mstr_datafolder + "Tiles/z_orthographic_" + self._latlngfld + "/orthos/" + str(self._ty) + "_" + str(self._tx) + ".dds") + # TODO: CUT OUT OCEANS AND SEAS IN DDS + os.remove(mstr_datafolder + "Tiles/z_orthographic_" + self._latlngfld + "/orthos/" + str(self._ty) + "_" + str(self._tx) + ".png") + + + + + # This checks the final image for empty patches. Should one be + # found, we will generate something to fill the gap. If this is + # the case, we will also note this in the database for the tile, + # under the special tag and value "tile", "completion". The same + # conditions apply for edge testing and so on. + def checkForEmptySpace(self): + empty = False + + # Load photo + layer_pix = self._tile.load() + + # Scan! + for y in range(self._tile.width-1): + for x in range(self._tile.height-1): + p = layer_pix[x,y] + if p[3] < 255: # <- Check for empty or non-complete alpha + empty = True + break + + # Tell about findings + return empty + + + # This returns a mask of the empty space to cover, should there be any + def buildCompletionMask(self): + mask = Image.new("RGBA", (self._imgsize, self._imgsize)) + mask_pix = mask.load() + + # Load photo + layer_pix = self._tile.load() + + # Scan! + for y in range(self._tile.width-1): + for x in range(self._tile.height-1): + p = layer_pix[x,y] + if p[3] < 255: # <- Check for empty or non-complete alpha + mask_pix[x,y] = (0,0,0,255) + # We do not apply any blur or other effects here - we only want the + # exact pixel positions. + + mask.save( mstr_datafolder + "_cache/" + str(self._lat) + "-" + str(self._ty) + "_" + str(self._lng) + "-" + str(self._tx) + "_tile-completion.png" ) + mstr_msg("photogen", "Generated and saved empty space mask") + + + # Construct a folder name for latitude and longitude + def latlng_folder(self, numbers): + fstr = "" + if numbers[0] >= 0: fstr = "+" + if numbers[0] < 0: fstr = "-" + if abs(numbers[0]) < 10: fstr = fstr + "0" + str(numbers[0]) + if abs(numbers[0]) >= 10 and numbers[0] <= 90: fstr = fstr + str(numbers[0]) + + if numbers[1] >= 0: fstr = fstr + "+" + if numbers[1] < 0: fstr = fstr + "-" + if abs(numbers[1]) < 10: fstr = fstr + "00" + str(numbers[1]) + if abs(numbers[1]) >= 10 and numbers[0] <= 99: fstr = fstr + "0" + str(numbers[1]) + if abs(numbers[1]) >= 100 : fstr = fstr + str(numbers[1]) + return fstr \ No newline at end of file diff --git a/repoinfo b/repoinfo index 4b37800..922834c 100644 --- a/repoinfo +++ b/repoinfo @@ -1,131 +1,131 @@ -[section]Orthographic[/section] - -A utility to generate photorealistic satellite imagery of any location in the world. Aimed to be an alternative for ortho photos in flight simulators, such as X-Plane or Microsoft Flight Simulator. Written in Python. - -While tools like Ortho4XP or AutoOrtho can provide next-level realism into X-Plane, the idea still falls short when it hits photos that were stitched together. In one or many ortho photos, you will see parts of the specific area of which the photo was not taken at the same time as another part, resulting in what is known as discoloration. Sometimes you will also find clouds in the photos, or remnants of buildings or attractions which show perspective distortion. This is a huge immersion-breaker for me and many others. A problem I wanted to tackle for my flight simulator - Orthographic may just be the solution that does away with all issues when it comes to ortho photos in flight simulators. - -Currently, three zoom levels are implemented, which are practically all cases suited well enough for the purpose of flight simulation: 16, 17, 18. I used zoom level 16 for areas I fly over, and 18 for areas close to an airport. - -[section]Wait, just what are ortho photos?[/section] - -Aerial photos taken with a satellite, of varying resolution, altitude, and detail. When you switch to satellite in Google or Bing Maps - what you see is a collage of ortho photos, depending on how close or far away you are zoomed in. - -[section]What it is[/section] - -As outlined, it is a tool that can generate, or rather - approximate, photo-realistic ortho photos. It does so by using a number of sophisticated mechanisms to generate these. These photos are then used as textures for the specific area the photo is made for - specifically a latitude and longitude coordinate. - -[section]What it is not[/section] - -It is obviously not a full replacement for the real thing - satellites provide photos of how the world really is. Orthographic can only approximate the looks, and it tries to do this as best as it can using the data it knows about the specific latitude and longitude... but do not expect a complete 1:1 match of the real world with fixed coloration. It is also not meant to compete with mapping services or tools - it is merely a collection of scripts working together to offer a close approximation of how a satellite photo would look like. - -[section]How does this work?[/section] - -It boils down into these steps: - -- Acquire OSM data for the tile from my API -- Construct masks from that data, for a number of layers, from top to bottom -- Covert each mask layer to a photo-realistic layer -- Merge (blend) all layers from bottom to top - -Orthographic creates zoom level 18 tiles for everything it encounters - but at the end, once all orthos are generated, zoom level 16 photos will be the final product as this is in almost all cases sufficient to fly in many altitudes. - -However, around airports, it will keep the original zoom level 18 tiles so that you have more detail when you fly into, and away from, airports. - -This is achieved by "memorizing" all airports with ICAO codes it encounters along its journey, then determining which ortho tiles to keep at zoom level 18. - - -[section]Important things to note[/section] - -Orthographic does its very best to approximate good-looking ortho photos, as if they were taken by a satellite. However it is what it is: an approximation - it cannot replace or even get close to the actual, real world. I have tried my very best to bring in some variety, and make it so that every part generated does not repeat or look the same - like it is the case with other ortho packages I have tried, apart from Ortho4XP / AutoOrtho. - -It is, at the end of the day, for a flight simulator. Orthographic can strike the right balance between accuracy of the part of the world you fly in, and looks. - -Prior to publishing, there have been many iterations and hours of testing the generation of buildings. While the current state is far from perfect, it is as best I can make it - and I think we can agree that it looks good enough... for flight simulators. With the right tools in place, buildings will most likely be placed on many of these locations anyways. - - -[section]Where it excels[/section] - -It does a really good job at generating good-looking photo tiles, and knows how to correct Mercator distortion to render these tiles. - -It also does a good job at making sure the tiles are seamless, and knows where a tile needs to be seamless, while at the same time, it also knows when it can pick something and be creative. - -During development it also learned how to make a few things look more realistic - namely (hopefully good enough, or believable) shadows for certain elements. - - -[section]Where it falls short[/section] - -Of course, an algorithm of this magnitude can only go so far. Also, this is a one-man project - which means I do not have the monetary resources to make this the ultimate ortho photo solution that works without a satellite... but still, I believe to have done a fairly good job - that is for you to decide. Our planet is vast and so is the data available for it - over time Orthographic will learn more and more how to make use of that data. - -In other words, it fails to generate parts of the world where there is no OSM data available. At this time, Orthographic gets creative to fill in the gaps. - -At this time, it also falls short in generating the 3D geometry for a tile. It is specialised to generate ortho photos - or in other words, textures for existing geometry. - -Buildings are usually generalized and show up with the same pattern. I need to work on that. - -Apart from that I am aware that the code is most likely not the best and can be optimized. It is my first serious Python project. - - -[section]Requirements[/section] - -- Current Python version -- Pillow for Python -- SQLite must be available to Python. On Windows it is by default, you may need to install the sqlite3 developer libraries on your distribution, then compile Python to automatically avail of this built-in module. - - -[section]Configuration[/section] - -- Open the file defines.py -- Change mstr_datafolder to where you want the data of Orthographic to be stored and placed -- Change mstr_airport_radius to an amount in zoom level 18 tiles. Areas around airports with ICAO code will get aerials with zoom level 18. I recommend to leave the default at 5 -- You can turn console logging on or off by changing the variable mstr_show_log - -In this file you can also define how large you want the final products to be - you can choose between 4k resolution (2048px) or 16k resolution (4096). The choice you make here should be based on 1) how much detail you want, and 2) how much VRAM your GPU has. The larger a texture, the more VRAM is needed, and the more processing is required by the GPU. I personally go with 2048, as I have a RTX2060, so my VRAM is limited. When at altitudes of 10,000 feet and above, you will most definitely not notice the difference between 4096 and 2048. - -NOTE! 4096 not yet implemented, but planned. - -Change the mstr_photores variable to 4096 if you want the maximum possible. - -Just a note: 4096 also uses 4x more hard drive space. 4k uses about 2MB per image, where as 16k uses about 8-10MB per image. This may not seem much for one image, but keep in mind we are talking about quite a considerable number of images. To get an idea - if you have it, look into any folder of an Ortho4XP tile in your X-Plane folder, and check the size of the "textures" folder. - -Also in defines.py, you will find the single layers, along with their corresponding mask blurring values. It is my strong recommendation NOT to change these values, as I have taken a lot of time to fine-tune these values. - - -[section]Running![/section] - -Very simple. - -[codebox]python og.py LATITUDE LONGITUDE[/codebox] - -So for example - -[codebox]python main.py 51 7[codebox] - -This does everything for you. - -ATTENTION! This process will take a considerable amount of time due to the work being involved. Please keep this in mind, especially if you seek to do this for an entire country. - -I am considering hosting the finished tiles on my server, so that you only need to download the finished photos / tile packs. - - -[section]Examples[/section] - -Dortmund, Germany - -[REAL WORLD] - -[RESULT] - - -Leverkusen, Germany - -[REAL WORLD] - -[RESULT] - - -[section]Improving the code[/section] - -As I have hosted this on my private but publicly accessible gitweb (which is the same tool kernel.org uses btw), it should be clear that I am making the code available to everyone. Of course, you are free to improve (I am sure the code needs some optimisation). If you changed or improved something, and you publish it (no matter where), you MUST adhere to the license this software ships with, which is OSL 3.0. - -As it is hosted on my private gitweb, and not in a public instance like GitHub, I need to be very careful and clear about this. In short it means, also legally, that you cannot download the current snapshot, post the content somewhere else, and claim you made it. Respect the time, work and energy I have invested into this project :) - +[section]Orthographic[/section] + +A utility to generate photorealistic satellite imagery of any location in the world. Aimed to be an alternative for ortho photos in flight simulators, such as X-Plane or Microsoft Flight Simulator. Written in Python. + +While tools like Ortho4XP or AutoOrtho can provide next-level realism into X-Plane, the idea still falls short when it hits photos that were stitched together. In one or many ortho photos, you will see parts of the specific area of which the photo was not taken at the same time as another part, resulting in what is known as discoloration. Sometimes you will also find clouds in the photos, or remnants of buildings or attractions which show perspective distortion. This is a huge immersion-breaker for me and many others. A problem I wanted to tackle for my flight simulator - Orthographic may just be the solution that does away with all issues when it comes to ortho photos in flight simulators. + +Currently, three zoom levels are implemented, which are practically all cases suited well enough for the purpose of flight simulation: 16, 17, 18. I used zoom level 16 for areas I fly over, and 18 for areas close to an airport. + +[section]Wait, just what are ortho photos?[/section] + +Aerial photos taken with a satellite, of varying resolution, altitude, and detail. When you switch to satellite in Google or Bing Maps - what you see is a collage of ortho photos, depending on how close or far away you are zoomed in. + +[section]What it is[/section] + +As outlined, it is a tool that can generate, or rather - approximate, photo-realistic ortho photos. It does so by using a number of sophisticated mechanisms to generate these. These photos are then used as textures for the specific area the photo is made for - specifically a latitude and longitude coordinate. + +[section]What it is not[/section] + +It is obviously not a full replacement for the real thing - satellites provide photos of how the world really is. Orthographic can only approximate the looks, and it tries to do this as best as it can using the data it knows about the specific latitude and longitude... but do not expect a complete 1:1 match of the real world with fixed coloration. It is also not meant to compete with mapping services or tools - it is merely a collection of scripts working together to offer a close approximation of how a satellite photo would look like. + +[section]How does this work?[/section] + +It boils down into these steps: + +- Acquire OSM data for the tile from my API +- Construct masks from that data, for a number of layers, from top to bottom +- Covert each mask layer to a photo-realistic layer +- Merge (blend) all layers from bottom to top + +Orthographic creates zoom level 18 tiles for everything it encounters - but at the end, once all orthos are generated, zoom level 16 photos will be the final product as this is in almost all cases sufficient to fly in many altitudes. + +However, around airports, it will keep the original zoom level 18 tiles so that you have more detail when you fly into, and away from, airports. + +This is achieved by "memorizing" all airports with ICAO codes it encounters along its journey, then determining which ortho tiles to keep at zoom level 18. + + +[section]Important things to note[/section] + +Orthographic does its very best to approximate good-looking ortho photos, as if they were taken by a satellite. However it is what it is: an approximation - it cannot replace or even get close to the actual, real world. I have tried my very best to bring in some variety, and make it so that every part generated does not repeat or look the same - like it is the case with other ortho packages I have tried, apart from Ortho4XP / AutoOrtho. + +It is, at the end of the day, for a flight simulator. Orthographic can strike the right balance between accuracy of the part of the world you fly in, and looks. + +Prior to publishing, there have been many iterations and hours of testing the generation of buildings. While the current state is far from perfect, it is as best I can make it - and I think we can agree that it looks good enough... for flight simulators. With the right tools in place, buildings will most likely be placed on many of these locations anyways. + + +[section]Where it excels[/section] + +It does a really good job at generating good-looking photo tiles, and knows how to correct Mercator distortion to render these tiles. + +It also does a good job at making sure the tiles are seamless, and knows where a tile needs to be seamless, while at the same time, it also knows when it can pick something and be creative. + +During development it also learned how to make a few things look more realistic - namely (hopefully good enough, or believable) shadows for certain elements. + + +[section]Where it falls short[/section] + +Of course, an algorithm of this magnitude can only go so far. Also, this is a one-man project - which means I do not have the monetary resources to make this the ultimate ortho photo solution that works without a satellite... but still, I believe to have done a fairly good job - that is for you to decide. Our planet is vast and so is the data available for it - over time Orthographic will learn more and more how to make use of that data. + +In other words, it fails to generate parts of the world where there is no OSM data available. At this time, Orthographic gets creative to fill in the gaps. + +At this time, it also falls short in generating the 3D geometry for a tile. It is specialised to generate ortho photos - or in other words, textures for existing geometry. + +Buildings are usually generalized and show up with the same pattern. I need to work on that. + +Apart from that I am aware that the code is most likely not the best and can be optimized. It is my first serious Python project. + + +[section]Requirements[/section] + +- Current Python version +- Pillow for Python +- SQLite must be available to Python. On Windows it is by default, you may need to install the sqlite3 developer libraries on your distribution, then compile Python to automatically avail of this built-in module. + + +[section]Configuration[/section] + +- Open the file defines.py +- Change mstr_datafolder to where you want the data of Orthographic to be stored and placed +- Change mstr_airport_radius to an amount in zoom level 18 tiles. Areas around airports with ICAO code will get aerials with zoom level 18. I recommend to leave the default at 5 +- You can turn console logging on or off by changing the variable mstr_show_log + +In this file you can also define how large you want the final products to be - you can choose between 4k resolution (2048px) or 16k resolution (4096). The choice you make here should be based on 1) how much detail you want, and 2) how much VRAM your GPU has. The larger a texture, the more VRAM is needed, and the more processing is required by the GPU. I personally go with 2048, as I have a RTX2060, so my VRAM is limited. When at altitudes of 10,000 feet and above, you will most definitely not notice the difference between 4096 and 2048. + +NOTE! 4096 not yet implemented, but planned. + +Change the mstr_photores variable to 4096 if you want the maximum possible. + +Just a note: 4096 also uses 4x more hard drive space. 4k uses about 2MB per image, where as 16k uses about 8-10MB per image. This may not seem much for one image, but keep in mind we are talking about quite a considerable number of images. To get an idea - if you have it, look into any folder of an Ortho4XP tile in your X-Plane folder, and check the size of the "textures" folder. + +Also in defines.py, you will find the single layers, along with their corresponding mask blurring values. It is my strong recommendation NOT to change these values, as I have taken a lot of time to fine-tune these values. + + +[section]Running![/section] + +Very simple. + +[codebox]python og.py LATITUDE LONGITUDE[/codebox] + +So for example + +[codebox]python main.py 51 7[codebox] + +This does everything for you. + +ATTENTION! This process will take a considerable amount of time due to the work being involved. Please keep this in mind, especially if you seek to do this for an entire country. + +I am considering hosting the finished tiles on my server, so that you only need to download the finished photos / tile packs. + + +[section]Examples[/section] + +Dortmund, Germany + +[REAL WORLD] + +[RESULT] + + +Leverkusen, Germany + +[REAL WORLD] + +[RESULT] + + +[section]Improving the code[/section] + +As I have hosted this on my private but publicly accessible gitweb (which is the same tool kernel.org uses btw), it should be clear that I am making the code available to everyone. Of course, you are free to improve (I am sure the code needs some optimisation). If you changed or improved something, and you publish it (no matter where), you MUST adhere to the license this software ships with, which is OSL 3.0. + +As it is hosted on my private gitweb, and not in a public instance like GitHub, I need to be very careful and clear about this. In short it means, also legally, that you cannot download the current snapshot, post the content somewhere else, and claim you made it. Respect the time, work and energy I have invested into this project :) + diff --git a/tiledb.py b/tiledb.py index b05c1ae..69e24d9 100644 --- a/tiledb.py +++ b/tiledb.py @@ -1,148 +1,148 @@ - -# ------------------------------------------------------------------- -# ORTHOGRAPHIC -# Your personal aerial satellite. Always on. At any altitude.* -# Developed by MarStrMind -# License: Open Software License 3.0 -# Up to date version always on marstr.online -# ------------------------------------------------------------------- -# tiledb.py -# Manages information concerning the information for a particular -# tile, particularly its consisting smaller subtiles. Used for -# consistent layer generation. -# ------------------------------------------------------------------- - -import sqlite3 -from defines import * -from functions import * -from log import * - -class mstr_tiledb: - def __init__(self, lat, lng, latlngfld): - # Note coords - self._latitude = lat - self._longitude = lng - self._latlngfld = latlngfld - - # The db file will be created, should it not exist - self._conn = sqlite3.connect(mstr_datafolder + "Tiles/z_orthographic_" + latlngfld + "/data.db") - self._crs = self._conn.cursor() - #mstr_msg("tiledb", "Database object initiated") - - - # Opens a database file - used by maskgen - def openDB(self, dbfile): - self._conn = sqlite3.connect(dbfile + ".db") - self._crs = self._conn.cursor() - - - # Creates tables for this particular degree of lat/lng - def create_tables(self): - self._conn.execute("CREATE TABLE IF NOT EXISTS tiledata (tile_v INTEGER, tile_h INTEGER, tag TEXT, value TEXT, source INTEGER, adjacent TEXT);") - self._conn.execute("CREATE TABLE IF NOT EXISTS airports (icao TEXT, tile_v INTEGER, tile_h INTEGER, latitude REAL, longitude REAL);") - self._conn.execute("CREATE TABLE IF NOT EXISTS completion (tile_v INTEGER, tile_h INTEGER, tag TEXT, value TEXT, source INTEGER, adjacent TEXT);") - - - # Insert data into their segments - def insert_info(self, tv, th, tag, value, src, adj): - self._conn.execute("INSERT INTO tiledata VALUES ( "+ str(tv) +", "+ str(th)+", '"+ tag +"', '"+ value +"', '"+ str(src) +"', '"+adj+"' );") - - # Insert data into the completion data - def insert_completion_info(self, tv, th, tag, value, src, adj): - self._conn.execute("INSERT INTO completion VALUES ( "+ str(tv) +", "+ str(th)+", '"+ tag +"', '"+ value +"', '"+ str(src) +"', '"+adj+"' );") - - # Insert an airport with ICAO code - def insert_icao(self, icao, tv, th, lat, lng): - self._conn.execute("INSERT INTO airports VALUES ('"+icao+"', "+str(tv)+", "+str(th)+", "+str(lat)+", "+str(lng)+");") - - # Commit a query or a number of queries - def commit_query(self): - self._conn.commit() - - - # Retrieve the adjacency string for normal tile generation - def get_adjacency_for_source(self, v, h, tag, value): - r = self._crs.execute("SELECT * FROM tiledata where tile_v="+str(v)+" and tile_h="+str(h)+" and tag='"+tag+"' and value='"+value+"';") - rws = r.fetchall() - return rws - - # Retrieve the adjacency info for completion of a tile - def get_adjacency_for_completion(self, v, h): - r = self._crs.execute("SELECT * FROM completion where tile_v="+str(v)+" and tile_h="+str(h)+";") - rws = r.fetchall() - return rws - - # These calls are a bit more complex. We also need to know adjacency info when we are directly - # next to another latitude or longitude. We want seamless generation in those situations too. - def get_adjacency_for_source_in_lat_lng(self, lat, lng, v, h, tag, value): - # The rows to return - rws = [] - - # For this we need to tap into the database of the tile, should there be one. - dbn = mstr_datafolder + "Tiles/" + str(lat) + "_" + str(lng) + "/data.db" - if os.path.isfile(dbn): - tileconn = sqlite3.connect(dbn) - tilecrsr = tileconn.cursor() - r = tilecrsr.execute("SELECT * from tiledata WHERE tile_v="+str(v)+" AND tile_h="+str(h)+" AND tag='"+tag+"' AND value='"+value+"';") - rws = r.fetchall() - tileconn.close() - - return rws - - # These calls are a bit more complex. We also need to know adjacency info when we are directly - # next to another latitude or longitude. We want seamless generation in those situations too. - def get_adjacency_for_completion_in_lat_lng(self, lat, lng, v, h): - # The rows to return - rws = [] - - # For this we need to tap into the database of the tile, should there be one. - dbn = mstr_datafolder + "Tiles/" + str(lat) + "_" + str(lng) + "/data.db" - if os.path.isfile(dbn): - tileconn = sqlite3.connect(dbn) - tilecrsr = tileconn.cursor() - r = tilecrsr.execute("SELECT * from completion WHERE tile_v="+str(v)+" AND tile_h="+str(h)+";") - rws = r.fetchall() - tileconn.close() - - return rws - - # Acquire the highest latitude and longitude tile number of a neighbouring tile - def get_highest_latlong_from_tile(self, lat, lng): - latlng = [-1, -1] - # For this we need to tap into the database of the tile, should there be one. - dbn = mstr_datafolder + "Tiles/" + str(lat) + "_" + str(lng) + "/data.db" - if os.path.isfile(dbn): - tileconn = sqlite3.connect(dbn) - tilecrsr = tileconn.cursor() - lt = tilecrsr.execute("SELECT * FROM tiledata ORDER BY tile_v DESC") - rws = lt.fetchall() - lat = rws[0][0] - ln = tilecrsr.execute("SELECT * FROM tiledata ORDER BY tile_h DESC") - rws = ln.fetchall() - lng = rws[0][1] - latlng[0] = lat - latlng[1] = lng - tileconn.close() - - return latlng - - - # Get all tiles with detected airports (ICAO codes) - def get_tiles_with_airports(self): - r = self._crs.execute("SELECT * FROM airports") - rws = r.fetchall - return rws - - - # Perform a custom query and retrieve results - def perform_query(self, qry): - r = self._crs.execute(qry) - rws = r.fetchall() - return rws - - - # Close DB - def close_db(self): - mstr_msg("tiledb", "Closing database connection") - self._conn.close() - + +# ------------------------------------------------------------------- +# ORTHOGRAPHIC +# Your personal aerial satellite. Always on. At any altitude.* +# Developed by MarStrMind +# License: Open Software License 3.0 +# Up to date version always on marstr.online +# ------------------------------------------------------------------- +# tiledb.py +# Manages information concerning the information for a particular +# tile, particularly its consisting smaller subtiles. Used for +# consistent layer generation. +# ------------------------------------------------------------------- + +import sqlite3 +from defines import * +from functions import * +from log import * + +class mstr_tiledb: + def __init__(self, lat, lng, latlngfld): + # Note coords + self._latitude = lat + self._longitude = lng + self._latlngfld = latlngfld + + # The db file will be created, should it not exist + self._conn = sqlite3.connect(mstr_datafolder + "Tiles/z_orthographic_" + latlngfld + "/data.db") + self._crs = self._conn.cursor() + #mstr_msg("tiledb", "Database object initiated") + + + # Opens a database file - used by maskgen + def openDB(self, dbfile): + self._conn = sqlite3.connect(dbfile + ".db") + self._crs = self._conn.cursor() + + + # Creates tables for this particular degree of lat/lng + def create_tables(self): + self._conn.execute("CREATE TABLE IF NOT EXISTS tiledata (tile_v INTEGER, tile_h INTEGER, tag TEXT, value TEXT, source INTEGER, adjacent TEXT);") + self._conn.execute("CREATE TABLE IF NOT EXISTS airports (icao TEXT, tile_v INTEGER, tile_h INTEGER, latitude REAL, longitude REAL);") + self._conn.execute("CREATE TABLE IF NOT EXISTS completion (tile_v INTEGER, tile_h INTEGER, tag TEXT, value TEXT, source INTEGER, adjacent TEXT);") + + + # Insert data into their segments + def insert_info(self, tv, th, tag, value, src, adj): + self._conn.execute("INSERT INTO tiledata VALUES ( "+ str(tv) +", "+ str(th)+", '"+ tag +"', '"+ value +"', '"+ str(src) +"', '"+adj+"' );") + + # Insert data into the completion data + def insert_completion_info(self, tv, th, tag, value, src, adj): + self._conn.execute("INSERT INTO completion VALUES ( "+ str(tv) +", "+ str(th)+", '"+ tag +"', '"+ value +"', '"+ str(src) +"', '"+adj+"' );") + + # Insert an airport with ICAO code + def insert_icao(self, icao, tv, th, lat, lng): + self._conn.execute("INSERT INTO airports VALUES ('"+icao+"', "+str(tv)+", "+str(th)+", "+str(lat)+", "+str(lng)+");") + + # Commit a query or a number of queries + def commit_query(self): + self._conn.commit() + + + # Retrieve the adjacency string for normal tile generation + def get_adjacency_for_source(self, v, h, tag, value): + r = self._crs.execute("SELECT * FROM tiledata where tile_v="+str(v)+" and tile_h="+str(h)+" and tag='"+tag+"' and value='"+value+"';") + rws = r.fetchall() + return rws + + # Retrieve the adjacency info for completion of a tile + def get_adjacency_for_completion(self, v, h): + r = self._crs.execute("SELECT * FROM completion where tile_v="+str(v)+" and tile_h="+str(h)+";") + rws = r.fetchall() + return rws + + # These calls are a bit more complex. We also need to know adjacency info when we are directly + # next to another latitude or longitude. We want seamless generation in those situations too. + def get_adjacency_for_source_in_lat_lng(self, lat, lng, v, h, tag, value): + # The rows to return + rws = [] + + # For this we need to tap into the database of the tile, should there be one. + dbn = mstr_datafolder + "Tiles/" + str(lat) + "_" + str(lng) + "/data.db" + if os.path.isfile(dbn): + tileconn = sqlite3.connect(dbn) + tilecrsr = tileconn.cursor() + r = tilecrsr.execute("SELECT * from tiledata WHERE tile_v="+str(v)+" AND tile_h="+str(h)+" AND tag='"+tag+"' AND value='"+value+"';") + rws = r.fetchall() + tileconn.close() + + return rws + + # These calls are a bit more complex. We also need to know adjacency info when we are directly + # next to another latitude or longitude. We want seamless generation in those situations too. + def get_adjacency_for_completion_in_lat_lng(self, lat, lng, v, h): + # The rows to return + rws = [] + + # For this we need to tap into the database of the tile, should there be one. + dbn = mstr_datafolder + "Tiles/" + str(lat) + "_" + str(lng) + "/data.db" + if os.path.isfile(dbn): + tileconn = sqlite3.connect(dbn) + tilecrsr = tileconn.cursor() + r = tilecrsr.execute("SELECT * from completion WHERE tile_v="+str(v)+" AND tile_h="+str(h)+";") + rws = r.fetchall() + tileconn.close() + + return rws + + # Acquire the highest latitude and longitude tile number of a neighbouring tile + def get_highest_latlong_from_tile(self, lat, lng): + latlng = [-1, -1] + # For this we need to tap into the database of the tile, should there be one. + dbn = mstr_datafolder + "Tiles/" + str(lat) + "_" + str(lng) + "/data.db" + if os.path.isfile(dbn): + tileconn = sqlite3.connect(dbn) + tilecrsr = tileconn.cursor() + lt = tilecrsr.execute("SELECT * FROM tiledata ORDER BY tile_v DESC") + rws = lt.fetchall() + lat = rws[0][0] + ln = tilecrsr.execute("SELECT * FROM tiledata ORDER BY tile_h DESC") + rws = ln.fetchall() + lng = rws[0][1] + latlng[0] = lat + latlng[1] = lng + tileconn.close() + + return latlng + + + # Get all tiles with detected airports (ICAO codes) + def get_tiles_with_airports(self): + r = self._crs.execute("SELECT * FROM airports") + rws = r.fetchall + return rws + + + # Perform a custom query and retrieve results + def perform_query(self, qry): + r = self._crs.execute(qry) + rws = r.fetchall() + return rws + + + # Close DB + def close_db(self): + mstr_msg("tiledb", "Closing database connection") + self._conn.close() + diff --git a/tilegen.py b/tilegen.py index 92e7040..f78769c 100644 --- a/tilegen.py +++ b/tilegen.py @@ -1,170 +1,170 @@ - -# ------------------------------------------------------------------- -# ORTHOGRAPHIC -# Your personal aerial satellite. Always on. At any altitude.* -# Developed by MarStrMind -# License: Open Software License 3.0 -# Up to date version always on marstr.online -# ------------------------------------------------------------------- -# tilegen.py -# Generates ZL16 tiles from the generated material, while checking on -# presence of airports, and keeping ZL18 tiles of the specified -# radius in defines.py. This also writes down the correspoding .ter -# files for X-Plane. Be careful: the unneeded ZL18 tiles will be -# removed. -# ------------------------------------------------------------------- - -from PIL import Image, ImageFilter -from log import * -from functions import * -from tiledb import * -import math -import os -import glob - -class mstr_tilegen: - # We only need some values. Also sets up connection to DB - def __init__(self, lat, lng, vstep, max_lat, max_lng): - self._lat = lat - self._lng = lng - self._vstep = vstep - self._maxlat = max_lat - self._maxlng = max_lng - # Connection to DB - self._tiledb = mstr_tiledb(lat, lng) - mstr_msg("tilegen", "Tilegen initialized") - - - # To write down X-Plane .ter files, we will need to know the exact size - # of the particular longitude we are in, as this value varies depending - # on where you are on a sphere. - # Returned values is in meters. - # The current latitude is needed. - def _findWidthOfLongitude(self, lat): - dm = math.cos(math.radians(lat)) * 111.321 # <- 1 deg width at equator in km - return round(dm * 1000, 3) - - - # Generates the ZL16 tiles and stores them. - # We generate ZL16 tiles first, then we check which tiles to keep near airports - def genTiles(self): - # The current lat and lng tile numbers - cur_lat = 1 - cur_lng = 1 - - # Actual starting coordinates for ZL16 - a_lat = self._lat + self._vstep * 2 - a_lng = self._lng + mstr_zl_18 * 2 - - # Scaled res - scaled_res = int(mstr_photores/4) # For example, 512 for a photo res of 2048 - - # Find out how many steps we can walk in every direction - steps_lat = int(math.ceil(self._maxlat/4)) - steps_lng = int(math.ceil(self._maxlng/4)) - mstr_msg("tilegen", "Latitude and longitude steps determined") - - # OK... so. Let's finish this. - for lt in range(1, steps_lat): - for ln in range(1, steps_lng): - # Check if we need to do something - if os.path.isfile(mstr_datafolder + "Tiles/" + str(self._lat) + "_" + str(self._lng) + "/textures/" + str(self._lat) + "-" + str(ln) + "_" + str(self._lng) + "-" + str(lt) + "_OG16.jpg") == False: - - mstr_msg("tilegen", "Generating missing zoom level 16 ortho " + str(self._lat) + "-" + str(ln) + "_" + str(self._lng) + "-" + str(lt) + "_OG16.jpg") - - # Find out which tiles to process - tiles = findZL16tiles(cur_lat, cur_lng) - - # Generate the ZL16 image - zl16 = Image.new("RGB", (mstr_photores, mstr_photores)) - - # Walk through this array - xpos = 0 - ypos = int(scaled_res*3) - for i in range(0, 3): - for j in range(0, 3): - # We may run into situations where ask for tiles that don't exist... - # Let's make sure we can continue - fpath = mstr_datafolder + "Tiles/" + str(self._lat) + "_" + str(self._lng) + "/textures/" + str(tiles[i][j][0]) + "_" + str(tiles[i][j][1]) + ".jpg" - if os.path.isfile( fpath ): - tlimg = Image.open(fpath) - tlimg = tlimg.resize((scaled_res,scaled_res), Image.Resampling.BILINEAR) - zl16.paste(tlimg, (xpos, ypos)) - xpos = xpos + scaled_res - xpos = 0 - ypos = ypos - scaled_res - - # Now save this image - zl16.save(mstr_datafolder + "Tiles/" + str(self._lat) + "_" + str(self._lng) + "/textures/" + str(self._lat) + "-" + str(ln) + "_" + str(self._lng) + "-" + str(lt) + "_OG16.jpg", format='JPEG', subsampling=0, quality=100) - - # Adjust - a_lng = a_lng + (mstr_zl_16 * 4) - cur_lng = cur_lng + 4 - mstr_msg("tilegen", "Adjusted coordinate values") - - # Adjust - a_lng = self._lat + (mstr_zl_16 * 2) - a_lat = a_lat + (self._vstep * 4) - cur_lat = cur_lat + 4 - cur_lng = self._lng - mstr_msg("tilegen", "Adjusted coordinate values for next tile loop") - - mstr_msg("tilegen", "Tile generation... completed (wow.jpg)") - - - # BUT! This is not the end. Yet. - - # Make sure we keep tiles around airports. - airports = self._tiledb.get_tiles_with_airports() - mstr_msg("tilegen", "Filtering ZL18 tiles for airports") - - # The ZL 18 tiles to keep in the end - tiles = [] - mstr_msg("tilegen", "Finding ZL18 tiles to keep") - for a in airports: - tiles.append(findAirportTiles(int(a[1]), int(a[2]))) - mstr_msg("tilegen", "Determined ZL18 tiles") - - # Create a final array to make life easier - mstr_msg("tilegen", "Generating arrays for tiles to keep") - keeping = [] - for t in tiles: - for i in t: - keeping.append(i) - - # Perform the cleanup - mstr_msg("tilegen", "Cleaning up non-needed tiles") - for y in range(1, self._maxlat): - for x in range(1, self._maxlng): - fn = str(y) + "_" + str(x) + ".jpg" - found = False - for k in keeping: - kfn = str(k[0]) + "_" + str(k[1]) + ".jpg" - if fn == kfn: - found = True - break - if found == False: - os.remove(mstr_datafolder + "/Tiles/" + str(self._lat) + "_" + str(self._lng) + "/textures/" + fn) - mstr_msg("tilegen", "Cleanup completed") - - mstr_msg("tilegen", "Work complete.") - - - -''' - Did we fly to the moon too soon? - Did we squander the chance? - In the rush of the race - The reason we chase is lost in romance - And still we try - To justify the waste - For a taste of man's greatest adventure - - I blame you for the moonlit sky - And the dream that died - With the eagles' flights - I blame you for the moonlit nights - When I wonder why - Are the seas still dry - Don't blame this sleeping satellite -''' + +# ------------------------------------------------------------------- +# ORTHOGRAPHIC +# Your personal aerial satellite. Always on. At any altitude.* +# Developed by MarStrMind +# License: Open Software License 3.0 +# Up to date version always on marstr.online +# ------------------------------------------------------------------- +# tilegen.py +# Generates ZL16 tiles from the generated material, while checking on +# presence of airports, and keeping ZL18 tiles of the specified +# radius in defines.py. This also writes down the correspoding .ter +# files for X-Plane. Be careful: the unneeded ZL18 tiles will be +# removed. +# ------------------------------------------------------------------- + +from PIL import Image, ImageFilter +from log import * +from functions import * +from tiledb import * +import math +import os +import glob + +class mstr_tilegen: + # We only need some values. Also sets up connection to DB + def __init__(self, lat, lng, vstep, max_lat, max_lng): + self._lat = lat + self._lng = lng + self._vstep = vstep + self._maxlat = max_lat + self._maxlng = max_lng + # Connection to DB + self._tiledb = mstr_tiledb(lat, lng) + mstr_msg("tilegen", "Tilegen initialized") + + + # To write down X-Plane .ter files, we will need to know the exact size + # of the particular longitude we are in, as this value varies depending + # on where you are on a sphere. + # Returned values is in meters. + # The current latitude is needed. + def _findWidthOfLongitude(self, lat): + dm = math.cos(math.radians(lat)) * 111.321 # <- 1 deg width at equator in km + return round(dm * 1000, 3) + + + # Generates the ZL16 tiles and stores them. + # We generate ZL16 tiles first, then we check which tiles to keep near airports + def genTiles(self): + # The current lat and lng tile numbers + cur_lat = 1 + cur_lng = 1 + + # Actual starting coordinates for ZL16 + a_lat = self._lat + self._vstep * 2 + a_lng = self._lng + mstr_zl_18 * 2 + + # Scaled res + scaled_res = int(mstr_photores/4) # For example, 512 for a photo res of 2048 + + # Find out how many steps we can walk in every direction + steps_lat = int(math.ceil(self._maxlat/4)) + steps_lng = int(math.ceil(self._maxlng/4)) + mstr_msg("tilegen", "Latitude and longitude steps determined") + + # OK... so. Let's finish this. + for lt in range(1, steps_lat): + for ln in range(1, steps_lng): + # Check if we need to do something + if os.path.isfile(mstr_datafolder + "Tiles/" + str(self._lat) + "_" + str(self._lng) + "/textures/" + str(self._lat) + "-" + str(ln) + "_" + str(self._lng) + "-" + str(lt) + "_OG16.jpg") == False: + + mstr_msg("tilegen", "Generating missing zoom level 16 ortho " + str(self._lat) + "-" + str(ln) + "_" + str(self._lng) + "-" + str(lt) + "_OG16.jpg") + + # Find out which tiles to process + tiles = findZL16tiles(cur_lat, cur_lng) + + # Generate the ZL16 image + zl16 = Image.new("RGB", (mstr_photores, mstr_photores)) + + # Walk through this array + xpos = 0 + ypos = int(scaled_res*3) + for i in range(0, 3): + for j in range(0, 3): + # We may run into situations where ask for tiles that don't exist... + # Let's make sure we can continue + fpath = mstr_datafolder + "Tiles/" + str(self._lat) + "_" + str(self._lng) + "/textures/" + str(tiles[i][j][0]) + "_" + str(tiles[i][j][1]) + ".jpg" + if os.path.isfile( fpath ): + tlimg = Image.open(fpath) + tlimg = tlimg.resize((scaled_res,scaled_res), Image.Resampling.BILINEAR) + zl16.paste(tlimg, (xpos, ypos)) + xpos = xpos + scaled_res + xpos = 0 + ypos = ypos - scaled_res + + # Now save this image + zl16.save(mstr_datafolder + "Tiles/" + str(self._lat) + "_" + str(self._lng) + "/textures/" + str(self._lat) + "-" + str(ln) + "_" + str(self._lng) + "-" + str(lt) + "_OG16.jpg", format='JPEG', subsampling=0, quality=100) + + # Adjust + a_lng = a_lng + (mstr_zl_16 * 4) + cur_lng = cur_lng + 4 + mstr_msg("tilegen", "Adjusted coordinate values") + + # Adjust + a_lng = self._lat + (mstr_zl_16 * 2) + a_lat = a_lat + (self._vstep * 4) + cur_lat = cur_lat + 4 + cur_lng = self._lng + mstr_msg("tilegen", "Adjusted coordinate values for next tile loop") + + mstr_msg("tilegen", "Tile generation... completed (wow.jpg)") + + + # BUT! This is not the end. Yet. + + # Make sure we keep tiles around airports. + airports = self._tiledb.get_tiles_with_airports() + mstr_msg("tilegen", "Filtering ZL18 tiles for airports") + + # The ZL 18 tiles to keep in the end + tiles = [] + mstr_msg("tilegen", "Finding ZL18 tiles to keep") + for a in airports: + tiles.append(findAirportTiles(int(a[1]), int(a[2]))) + mstr_msg("tilegen", "Determined ZL18 tiles") + + # Create a final array to make life easier + mstr_msg("tilegen", "Generating arrays for tiles to keep") + keeping = [] + for t in tiles: + for i in t: + keeping.append(i) + + # Perform the cleanup + mstr_msg("tilegen", "Cleaning up non-needed tiles") + for y in range(1, self._maxlat): + for x in range(1, self._maxlng): + fn = str(y) + "_" + str(x) + ".jpg" + found = False + for k in keeping: + kfn = str(k[0]) + "_" + str(k[1]) + ".jpg" + if fn == kfn: + found = True + break + if found == False: + os.remove(mstr_datafolder + "/Tiles/" + str(self._lat) + "_" + str(self._lng) + "/textures/" + fn) + mstr_msg("tilegen", "Cleanup completed") + + mstr_msg("tilegen", "Work complete.") + + + +''' + Did we fly to the moon too soon? + Did we squander the chance? + In the rush of the race + The reason we chase is lost in romance + And still we try + To justify the waste + For a taste of man's greatest adventure + + I blame you for the moonlit sky + And the dream that died + With the eagles' flights + I blame you for the moonlit nights + When I wonder why + Are the seas still dry + Don't blame this sleeping satellite +''' diff --git a/xp_dsfgen.py b/xp_dsfgen.py index d764230..b05483f 100644 --- a/xp_dsfgen.py +++ b/xp_dsfgen.py @@ -1,211 +1,211 @@ - -# ------------------------------------------------------------------- -# ORTHOGRAPHIC -# Your personal aerial satellite. Always on. At any altitude.* -# Developed by MarStrMind -# License: Open Software License 3.0 -# Up to date version always on marstr.online -# ------------------------------------------------------------------- -# xp_dsfgen.py -# This class is coming into play at the very end of the tile -# generation process, and builds the DSF (Distributable Scenery -# Format) file for X-Plane. -# -# For this, you will need DSFTool which I cannot re-distribute. -# -# You can download it for free from X-Plane's website. -# Place them somewhere convenient, and point to them in the -# xp_ variables in defines.py -# ------------------------------------------------------------------- - -import os -import glob -import math -from random import randrange -from log import * - -class mstr_xp_dsfgen: - # Instantiate with Lat/Lng, as usual - def __init__(self, lat, lng, mlat, mlng, vstep): - self._latitude = lat - self._longitude = lng - self._maxlat = mlat - self._maxlng = mlng - self._tmpdsf = mstr_datafolder + "_cache/tiledsf.txt" - self._vstep = vstep - self._dsfstring = "" - mstr_msg("xp_dsfgen", "[X-Plane] DSFgen initialized") - self.build_header() - - - # Construct header of DSF txt - def build_header(self): - self._dsfstring = self._dsfstring + "PROPERTY sim/west " + str(int(self._longitude)) + "\n" - self._dsfstring = self._dsfstring + "PROPERTY sim/east " + str(int(self._longitude + 1)) + "\n" - self._dsfstring = self._dsfstring + "PROPERTY sim/south " + str(int(self._latitude)) + "\n" - self._dsfstring = self._dsfstring + "PROPERTY sim/north " + str(int(self._latitude+1)) + "\n" - self._dsfstring = self._dsfstring + "PROPERTY sim/planet earth\n" - self._dsfstring = self._dsfstring + "PROPERTY sim/creation_agent Orthographic\n" - self._dsfstring = self._dsfstring + "PROPERTY sim/overlay 1\n" # <- DO NOT REMOVE THIS!! - self._dsfstring = self._dsfstring + "PROPERTY sim/require_facade 6/0\n" - mstr_msg("xp_dsfgen", "[X-Plane] DSF header built") - - - # Write the text file - def write_dsf_txt(self): - mstr_msg("xp_dsfgen", "[X-Plane] Writing DSF txt file") - with open(self._tmpdsf, 'w') as textfile: - textfile.write(self._dsfstring) - - - # Convert the DSF into actual, usable data for X-Plane - def convert_dsf_text(self): - mstr_msg("xp_dsfgen", "[X-Plane] Converting DSF information into X-Plane DSF file") - # Find separator - sep = "" - if os.name == "nt": - sep = "\\" - if os.name == "posix": - sep = "/" - - datafolder = mstr_datafolder.replace("/", sep) - - # First, create the Earth nav data folder should it not exist - end_base = datafolder + "Tiles/z_orthographic_" + self.xplane_latlng_folder([self._latitude, self._longitude]) + sep + "Earth nav data" - - # Create the appropriate rounded folder - end_round = self.xplane_latlng_folder(self.find_earthnavdata_number()) - if not os.path.exists(end_base): - os.makedirs(end_base) - if not os.path.exists(end_base + sep + end_round): - os.makedirs(end_base + sep + end_round) - - # Get the file name for the DSF - end_latlng = self.xplane_latlng_folder([self._latitude, self._longitude]) - - # Perform conversion - os.system(mstr_xp_dsftool + " --text2dsf " + datafolder + "_cache" + sep + "tiledsf.txt \"" + end_base + sep + end_round + sep + end_latlng + ".dsf\"") - mstr_msg("xp_dsfgen", "[X-Plane] DSF conversion complete") - - - # Find the next "by-ten" numbers for the current latitude and longitude - def find_earthnavdata_number(self): - earthnavdata = [] - lat = abs(int(self._latitude / 10) * 10) - lng = abs(int(self._longitude / 10) * 10) - earthnavdata.append(lat) - earthnavdata.append(lng) - return earthnavdata - - # Find with of a longitude (needed for DSF and .pol files) - def _findWidthOfLongitude(self, lat): - dm = math.cos(math.radians(lat)) * 111.321 # <- 1 deg width at equator in km - return round(dm * 1000, 3) - - # Find diameter of an ortho - def find_ortho_diameter(self, side): - sq = side * side - sqsum = sq + sq - dm = math.sqrt(sqsum) - - - # Construct an X-Plane compatible folder name for latitude and longitude - def xplane_latlng_folder(self, numbers): - fstr = "" - if numbers[0] >= 0: fstr = "+" - if numbers[0] < 0: fstr = "-" - if abs(numbers[0]) < 10: fstr = fstr + "0" + str(numbers[0]) - if abs(numbers[0]) >= 10 and numbers[0] <= 90: fstr = fstr + str(numbers[0]) - - if numbers[1] >= 0: fstr = fstr + "+" - if numbers[1] < 0: fstr = fstr + "-" - if abs(numbers[1]) < 10: fstr = fstr + "00" + str(numbers[1]) - if abs(numbers[1]) >= 10 and numbers[0] <= 99: fstr = fstr + "0" + str(numbers[1]) - if abs(numbers[1]) >= 100 : fstr = fstr + str(numbers[1]) - - return fstr - - - # Build the complete DSF. - # This is the main function to call. - def build_dsf_for_tile(self): - mstr_msg("xp_dsfgen", "[X-Plane] Building DSF file") - # Add the polygon definition file entries - for v in range(1, self._maxlat+1): - for h in range(1, self._maxlng+1): - self._dsfstring = self._dsfstring + "POLYGON_DEF terrain/"+str(v)+"_"+str(h)+".pol\n" - - # Add the definitions for each ortho tile - curpol = 0 - cur_lat = self._latitude - cur_lng = self._longitude - for v in range(1, self._maxlat+1): - for h in range(1, self._maxlng+1): - bbox = [ cur_lat, cur_lng, cur_lat+self._vstep-0.00001, cur_lng + mstr_zl_18-0.00001 ] - self._dsfstring = self._dsfstring + "BEGIN_POLYGON "+str(curpol)+" 65535 4\n" - self._dsfstring = self._dsfstring + "BEGIN_WINDING\n" - - self._dsfstring = self._dsfstring + "POLYGON_POINT " + str(bbox[1]) + " " + str(bbox[0]) + " 0.000000000 0.000000000\n" - self._dsfstring = self._dsfstring + "POLYGON_POINT " + str(bbox[3]) + " " + str(bbox[0]) + " 1.000000000 0.000000000\n" - self._dsfstring = self._dsfstring + "POLYGON_POINT " + str(bbox[3]) + " " + str(bbox[2]) + " 1.000000000 1.000000000\n" - self._dsfstring = self._dsfstring + "POLYGON_POINT " + str(bbox[1]) + " " + str(bbox[2]) + " 0.000000000 1.000000000\n" - - self._dsfstring = self._dsfstring + "END_WINDING\n" - self._dsfstring = self._dsfstring + "END_POLYGON\n" - - # Adjust forward - cur_lng = cur_lng + mstr_zl_18 - curpol = curpol + 1 - - # Adjust up, reset longitude - cur_lng = self._longitude - cur_lat = cur_lat + self._vstep - - # OK... we can now save this - self.write_dsf_txt() - - # Generate the single .pol files now - mstr_msg("xp_dsfgen", "[X-Plane] Beginning generation of terrain/*.pol files") - cur_lat = self._latitude - cur_lng = self._longitude - lclat = cur_lat + (self._vstep/2) - lclng = cur_lng + (mstr_zl_18/2) - for v in range(1, self._maxlat+1): - for h in range(1, self._maxlng+1): - dm = self._findWidthOfLongitude(cur_lng) * mstr_zl_18 - dg = self.find_ortho_diameter(dm) - polstr = "" - polstr = polstr + "A\n" - polstr = polstr + "850\n" - polstr = polstr + "DRAPED_POLYGON\n" - polstr = polstr + "\n" - polstr = polstr + "TEXTURE_NOWRAP ../orthos/"+str(v)+"_"+str(h)+".dds\n" - - # Check for existence of a normal map - # If there is one, we will add that too - end_latlng = self.xplane_latlng_folder([self._latitude, self._longitude]) - if os.path.isfile(mstr_datafolder + "Tiles/z_orthographic_"+end_latlng+"/normals/" + str(v) + "_" + str(h) + ".png") == True: - polstr = polstr + "TEXTURE_NORMAL 1 ../normals/"+str(v)+"_"+str(h)+".png\n" - - polstr = polstr + "SCALE "+str(dm)+" "+str(dm)+"\n" - polstr = polstr + "LOAD_CENTER "+str(lclat)+" " +str(lclng)+" " +str(dg)+ " " +str(mstr_photores)+"\n" - polstr = polstr + "LAYER_GROUP TERRAIN 1\n" - - # Save this content - with open(mstr_datafolder + "Tiles/z_orthographic_"+end_latlng+"/terrain/"+str(v)+"_"+str(h)+".pol", 'w') as textfile: - textfile.write(polstr) - - # Adjust forward - cur_lng = cur_lng + mstr_zl_18 - lclng = cur_lng + (mstr_zl_18/2) - - cur_lng = self._longitude - lclng = cur_lng + (mstr_zl_18/2) - - cur_lat = cur_lat + self._vstep - lclat = cur_lat + (self._vstep/2) - - mstr_msg("xp_dsfgen", "[X-Plane] Converting DSF txt") - self.convert_dsf_text() - - mstr_msg("xp_dsfgen", "[X-Plane] DSF for tile completed") + +# ------------------------------------------------------------------- +# ORTHOGRAPHIC +# Your personal aerial satellite. Always on. At any altitude.* +# Developed by MarStrMind +# License: Open Software License 3.0 +# Up to date version always on marstr.online +# ------------------------------------------------------------------- +# xp_dsfgen.py +# This class is coming into play at the very end of the tile +# generation process, and builds the DSF (Distributable Scenery +# Format) file for X-Plane. +# +# For this, you will need DSFTool which I cannot re-distribute. +# +# You can download it for free from X-Plane's website. +# Place them somewhere convenient, and point to them in the +# xp_ variables in defines.py +# ------------------------------------------------------------------- + +import os +import glob +import math +from random import randrange +from log import * + +class mstr_xp_dsfgen: + # Instantiate with Lat/Lng, as usual + def __init__(self, lat, lng, mlat, mlng, vstep): + self._latitude = lat + self._longitude = lng + self._maxlat = mlat + self._maxlng = mlng + self._tmpdsf = mstr_datafolder + "_cache/tiledsf.txt" + self._vstep = vstep + self._dsfstring = "" + mstr_msg("xp_dsfgen", "[X-Plane] DSFgen initialized") + self.build_header() + + + # Construct header of DSF txt + def build_header(self): + self._dsfstring = self._dsfstring + "PROPERTY sim/west " + str(int(self._longitude)) + "\n" + self._dsfstring = self._dsfstring + "PROPERTY sim/east " + str(int(self._longitude + 1)) + "\n" + self._dsfstring = self._dsfstring + "PROPERTY sim/south " + str(int(self._latitude)) + "\n" + self._dsfstring = self._dsfstring + "PROPERTY sim/north " + str(int(self._latitude+1)) + "\n" + self._dsfstring = self._dsfstring + "PROPERTY sim/planet earth\n" + self._dsfstring = self._dsfstring + "PROPERTY sim/creation_agent Orthographic\n" + self._dsfstring = self._dsfstring + "PROPERTY sim/overlay 1\n" # <- DO NOT REMOVE THIS!! + self._dsfstring = self._dsfstring + "PROPERTY sim/require_facade 6/0\n" + mstr_msg("xp_dsfgen", "[X-Plane] DSF header built") + + + # Write the text file + def write_dsf_txt(self): + mstr_msg("xp_dsfgen", "[X-Plane] Writing DSF txt file") + with open(self._tmpdsf, 'w') as textfile: + textfile.write(self._dsfstring) + + + # Convert the DSF into actual, usable data for X-Plane + def convert_dsf_text(self): + mstr_msg("xp_dsfgen", "[X-Plane] Converting DSF information into X-Plane DSF file") + # Find separator + sep = "" + if os.name == "nt": + sep = "\\" + if os.name == "posix": + sep = "/" + + datafolder = mstr_datafolder.replace("/", sep) + + # First, create the Earth nav data folder should it not exist + end_base = datafolder + "Tiles/z_orthographic_" + self.xplane_latlng_folder([self._latitude, self._longitude]) + sep + "Earth nav data" + + # Create the appropriate rounded folder + end_round = self.xplane_latlng_folder(self.find_earthnavdata_number()) + if not os.path.exists(end_base): + os.makedirs(end_base) + if not os.path.exists(end_base + sep + end_round): + os.makedirs(end_base + sep + end_round) + + # Get the file name for the DSF + end_latlng = self.xplane_latlng_folder([self._latitude, self._longitude]) + + # Perform conversion + os.system(mstr_xp_dsftool + " --text2dsf " + datafolder + "_cache" + sep + "tiledsf.txt \"" + end_base + sep + end_round + sep + end_latlng + ".dsf\"") + mstr_msg("xp_dsfgen", "[X-Plane] DSF conversion complete") + + + # Find the next "by-ten" numbers for the current latitude and longitude + def find_earthnavdata_number(self): + earthnavdata = [] + lat = abs(int(self._latitude / 10) * 10) + lng = abs(int(self._longitude / 10) * 10) + earthnavdata.append(lat) + earthnavdata.append(lng) + return earthnavdata + + # Find with of a longitude (needed for DSF and .pol files) + def _findWidthOfLongitude(self, lat): + dm = math.cos(math.radians(lat)) * 111.321 # <- 1 deg width at equator in km + return round(dm * 1000, 3) + + # Find diameter of an ortho + def find_ortho_diameter(self, side): + sq = side * side + sqsum = sq + sq + dm = math.sqrt(sqsum) + + + # Construct an X-Plane compatible folder name for latitude and longitude + def xplane_latlng_folder(self, numbers): + fstr = "" + if numbers[0] >= 0: fstr = "+" + if numbers[0] < 0: fstr = "-" + if abs(numbers[0]) < 10: fstr = fstr + "0" + str(numbers[0]) + if abs(numbers[0]) >= 10 and numbers[0] <= 90: fstr = fstr + str(numbers[0]) + + if numbers[1] >= 0: fstr = fstr + "+" + if numbers[1] < 0: fstr = fstr + "-" + if abs(numbers[1]) < 10: fstr = fstr + "00" + str(numbers[1]) + if abs(numbers[1]) >= 10 and numbers[0] <= 99: fstr = fstr + "0" + str(numbers[1]) + if abs(numbers[1]) >= 100 : fstr = fstr + str(numbers[1]) + + return fstr + + + # Build the complete DSF. + # This is the main function to call. + def build_dsf_for_tile(self): + mstr_msg("xp_dsfgen", "[X-Plane] Building DSF file") + # Add the polygon definition file entries + for v in range(1, self._maxlat+1): + for h in range(1, self._maxlng+1): + self._dsfstring = self._dsfstring + "POLYGON_DEF terrain/"+str(v)+"_"+str(h)+".pol\n" + + # Add the definitions for each ortho tile + curpol = 0 + cur_lat = self._latitude + cur_lng = self._longitude + for v in range(1, self._maxlat+1): + for h in range(1, self._maxlng+1): + bbox = [ cur_lat, cur_lng, cur_lat+self._vstep-0.00001, cur_lng + mstr_zl_18-0.00001 ] + self._dsfstring = self._dsfstring + "BEGIN_POLYGON "+str(curpol)+" 65535 4\n" + self._dsfstring = self._dsfstring + "BEGIN_WINDING\n" + + self._dsfstring = self._dsfstring + "POLYGON_POINT " + str(bbox[1]) + " " + str(bbox[0]) + " 0.000000000 0.000000000\n" + self._dsfstring = self._dsfstring + "POLYGON_POINT " + str(bbox[3]) + " " + str(bbox[0]) + " 1.000000000 0.000000000\n" + self._dsfstring = self._dsfstring + "POLYGON_POINT " + str(bbox[3]) + " " + str(bbox[2]) + " 1.000000000 1.000000000\n" + self._dsfstring = self._dsfstring + "POLYGON_POINT " + str(bbox[1]) + " " + str(bbox[2]) + " 0.000000000 1.000000000\n" + + self._dsfstring = self._dsfstring + "END_WINDING\n" + self._dsfstring = self._dsfstring + "END_POLYGON\n" + + # Adjust forward + cur_lng = cur_lng + mstr_zl_18 + curpol = curpol + 1 + + # Adjust up, reset longitude + cur_lng = self._longitude + cur_lat = cur_lat + self._vstep + + # OK... we can now save this + self.write_dsf_txt() + + # Generate the single .pol files now + mstr_msg("xp_dsfgen", "[X-Plane] Beginning generation of terrain/*.pol files") + cur_lat = self._latitude + cur_lng = self._longitude + lclat = cur_lat + (self._vstep/2) + lclng = cur_lng + (mstr_zl_18/2) + for v in range(1, self._maxlat+1): + for h in range(1, self._maxlng+1): + dm = self._findWidthOfLongitude(cur_lng) * mstr_zl_18 + dg = self.find_ortho_diameter(dm) + polstr = "" + polstr = polstr + "A\n" + polstr = polstr + "850\n" + polstr = polstr + "DRAPED_POLYGON\n" + polstr = polstr + "\n" + polstr = polstr + "TEXTURE_NOWRAP ../orthos/"+str(v)+"_"+str(h)+".dds\n" + + # Check for existence of a normal map + # If there is one, we will add that too + end_latlng = self.xplane_latlng_folder([self._latitude, self._longitude]) + if os.path.isfile(mstr_datafolder + "Tiles/z_orthographic_"+end_latlng+"/normals/" + str(v) + "_" + str(h) + ".png") == True: + polstr = polstr + "TEXTURE_NORMAL 1 ../normals/"+str(v)+"_"+str(h)+".png\n" + + polstr = polstr + "SCALE "+str(dm)+" "+str(dm)+"\n" + polstr = polstr + "LOAD_CENTER "+str(lclat)+" " +str(lclng)+" " +str(dg)+ " " +str(mstr_photores)+"\n" + polstr = polstr + "LAYER_GROUP TERRAIN 1\n" + + # Save this content + with open(mstr_datafolder + "Tiles/z_orthographic_"+end_latlng+"/terrain/"+str(v)+"_"+str(h)+".pol", 'w') as textfile: + textfile.write(polstr) + + # Adjust forward + cur_lng = cur_lng + mstr_zl_18 + lclng = cur_lng + (mstr_zl_18/2) + + cur_lng = self._longitude + lclng = cur_lng + (mstr_zl_18/2) + + cur_lat = cur_lat + self._vstep + lclat = cur_lat + (self._vstep/2) + + mstr_msg("xp_dsfgen", "[X-Plane] Converting DSF txt") + self.convert_dsf_text() + + mstr_msg("xp_dsfgen", "[X-Plane] DSF for tile completed") diff --git a/xp_normalmap.py b/xp_normalmap.py index 56996c3..511090d 100644 --- a/xp_normalmap.py +++ b/xp_normalmap.py @@ -1,172 +1,172 @@ - -# ------------------------------------------------------------------- -# ORTHOGRAPHIC -# Your personal aerial satellite. Always on. At any altitude.* -# Developed by MarStrMind -# License: Open Software License 3.0 -# Up to date version always on marstr.online -# ------------------------------------------------------------------- -# xp_normalmap.py -# For some geographical features, we will add a normal map for -# additional realism in the orthos, without using additional meshes -# or geometry. -# -# Source converted to Python from this C++ StackOverflow: -# https://stackoverflow.com/a/2368794 -# ------------------------------------------------------------------- - - -import numpy as np -import math -import os -from PIL import Image, ImageFilter -from defines import * -from log import * - - -class mstr_xp_normalmap: - - # Only a few params - def __init__(self, lat, lng, tag, value, tv, th, latlngfld): - self._lat = lat - self._lng = lng - self._tag = tag - self._value = value - self._latlngfld = latlngfld - self._tv = tv - self._th = th - mstr_msg("xp_normalmap", "[X-Plane] Normal Map generator initialized") - - - # Load the layer image and resize it to 1/4th its size - - # then provide it - def load_layer(self): - qtr = int(mstr_photores / 4) - image = Image.open(mstr_datafolder + "_cache/" + str(self._lat) + "-" + str(self._tv) + "_" + str(self._lng) + "-" + str(self._th) + "_" + self._tag + "-" + self._value + "_layer.png") - image = image.resize((qtr,qtr), Image.Resampling.LANCZOS) - mstr_msg("xp_normalmap", "[X-Plane] Layer image loaded") - return image - - - # A few mathematical calls we need - # -------------------------------------------------------- - def intensity(self, pixel): - avg = (pixel[0] + pixel[1] + pixel[2]) / 3 - if avg > 0: - pavg = 255.0 / avg - else: - pavg = 0 - return pavg - - - def clamp(self, px, mpx): - if px > mpx-1: - return mpx-1 - else: - if px < 0: - return 0 - else: - return px - - - def map_component(self, px): - return (px + 1.0) * (255.0 / 2.0) - - - def normalize_vector(self, v): - vc = np.array([v[0], v[1], v[2]]) - norm = np.linalg.norm(vc) - nv = vc / norm - return nv - # -------------------------------------------------------- - - - # The Big Mac. Generate the normal map - def generate_normal_map_for_layer(self, image): - mstr_msg("xp_normalmap", "[X-Plane] Beginning normal map generation") - #nmp = Image.new("RGBA", (image.width, image.height), (128,128,255,255)) - nmp = Image.new("RGBA", (image.width, image.height), (0,0,0,0)) - org = image.load() - nmp_pix = nmp.load() - - # Find out which alpha value to use - alpha = 0 - for n in mstr_xp_normal_maps: - if n[0] == self._tag and n[1] == self._value: - v = n[2] / 100 - a = v * 255.0 - alpha = int(a) - - - # Let's try some shenanigans - w = image.width - h = image.height - for y in range(h): - for x in range(w): - p = org[x,y] - if p[3] > 0: # Only do something if there is something to do in layer - # Neighboring pixels - px_t = org[ self.clamp(x, w), self.clamp(y+1, h) ] - px_tr = org[ self.clamp(x+1, w), self.clamp(y+1, h) ] - px_r = org[ self.clamp(x+1, w), self.clamp(y, h) ] - px_br = org[ self.clamp(x+1, w), self.clamp(y+1, h) ] - px_b = org[ self.clamp(x, w), self.clamp(y-1, h) ] - px_bl = org[ self.clamp(x-1, w), self.clamp(y-1, h) ] - px_l = org[ self.clamp(x-1, w), self.clamp(y, h) ] - px_tl = org[ self.clamp(x-1, w), self.clamp(y+1, h) ] - - # Intensities of pixels - it_t = self.intensity(px_t) - it_tr = self.intensity(px_tr) - it_r = self.intensity(px_r) - it_br = self.intensity(px_br) - it_b = self.intensity(px_b) - it_bl = self.intensity(px_bl) - it_l = self.intensity(px_l) - it_tl = self.intensity(px_tl) - - # Sobel filter - dx = (it_tr + 2.0 * it_r + it_br) - (it_tl + 2.0 * it_l + it_bl) - dy = (it_bl + 2.0 * it_b + it_br) - (it_tl + 2.0 * it_t + it_tr) - dz = 10 # This is usually a good value for strength - v = (dx, dy, dz) - nrm = self.normalize_vector(v) - - # Invert height for our Orthos - if nrm[1] > 0: - nrm[1] = 0 - (abs(nrm[1])) - else: - nrm[1] = abs(nrm[1]) - - # Set pixel - nmp_pix[x,y] = (int(self.map_component(nrm[0])), int(self.map_component(nrm[1])), int(self.map_component(nrm[2])), alpha) - - mstr_msg("xp_normalmap", "[X-Plane] Normal map generated") - return nmp - - - # The funnction to call. Blends with the existing map, or creates a new one - def build_normalmap(self): - mstr_msg("xp_normalmap", "[X-Plane] Building normal map") - # The layer image - lyr = self.load_layer() - - # Make the normal map for the layer - nrm = self.generate_normal_map_for_layer(lyr) - - # Normal map final file name - nrmfln = mstr_datafolder + "Tiles/z_orthographic_" + self._latlngfld + "/normals/" + str(self._tv) + "_" + str(self._th) + ".png" - - # Check for existence of normal map file - ex = os.path.isfile(nrmfln) - - # Does not exist? Just save - if ex == False: - nrm.save(nrmfln) - - # Exists? Open it, composite both, save - if ex == True: - nrmmap = Image.open(nrmfln) - nrmmap.alpha_composite(nrm) - nrmmap.save(nrmfln) - mstr_msg("xp_normalmap", "[X-Plane] Normal map saved") + +# ------------------------------------------------------------------- +# ORTHOGRAPHIC +# Your personal aerial satellite. Always on. At any altitude.* +# Developed by MarStrMind +# License: Open Software License 3.0 +# Up to date version always on marstr.online +# ------------------------------------------------------------------- +# xp_normalmap.py +# For some geographical features, we will add a normal map for +# additional realism in the orthos, without using additional meshes +# or geometry. +# +# Source converted to Python from this C++ StackOverflow: +# https://stackoverflow.com/a/2368794 +# ------------------------------------------------------------------- + + +import numpy as np +import math +import os +from PIL import Image, ImageFilter +from defines import * +from log import * + + +class mstr_xp_normalmap: + + # Only a few params + def __init__(self, lat, lng, tag, value, tv, th, latlngfld): + self._lat = lat + self._lng = lng + self._tag = tag + self._value = value + self._latlngfld = latlngfld + self._tv = tv + self._th = th + mstr_msg("xp_normalmap", "[X-Plane] Normal Map generator initialized") + + + # Load the layer image and resize it to 1/4th its size - + # then provide it + def load_layer(self): + qtr = int(mstr_photores / 4) + image = Image.open(mstr_datafolder + "_cache/" + str(self._lat) + "-" + str(self._tv) + "_" + str(self._lng) + "-" + str(self._th) + "_" + self._tag + "-" + self._value + "_layer.png") + image = image.resize((qtr,qtr), Image.Resampling.LANCZOS) + mstr_msg("xp_normalmap", "[X-Plane] Layer image loaded") + return image + + + # A few mathematical calls we need + # -------------------------------------------------------- + def intensity(self, pixel): + avg = (pixel[0] + pixel[1] + pixel[2]) / 3 + if avg > 0: + pavg = 255.0 / avg + else: + pavg = 0 + return pavg + + + def clamp(self, px, mpx): + if px > mpx-1: + return mpx-1 + else: + if px < 0: + return 0 + else: + return px + + + def map_component(self, px): + return (px + 1.0) * (255.0 / 2.0) + + + def normalize_vector(self, v): + vc = np.array([v[0], v[1], v[2]]) + norm = np.linalg.norm(vc) + nv = vc / norm + return nv + # -------------------------------------------------------- + + + # The Big Mac. Generate the normal map + def generate_normal_map_for_layer(self, image): + mstr_msg("xp_normalmap", "[X-Plane] Beginning normal map generation") + #nmp = Image.new("RGBA", (image.width, image.height), (128,128,255,255)) + nmp = Image.new("RGBA", (image.width, image.height), (0,0,0,0)) + org = image.load() + nmp_pix = nmp.load() + + # Find out which alpha value to use + alpha = 0 + for n in mstr_xp_normal_maps: + if n[0] == self._tag and n[1] == self._value: + v = n[2] / 100 + a = v * 255.0 + alpha = int(a) + + + # Let's try some shenanigans + w = image.width + h = image.height + for y in range(h): + for x in range(w): + p = org[x,y] + if p[3] > 0: # Only do something if there is something to do in layer + # Neighboring pixels + px_t = org[ self.clamp(x, w), self.clamp(y+1, h) ] + px_tr = org[ self.clamp(x+1, w), self.clamp(y+1, h) ] + px_r = org[ self.clamp(x+1, w), self.clamp(y, h) ] + px_br = org[ self.clamp(x+1, w), self.clamp(y+1, h) ] + px_b = org[ self.clamp(x, w), self.clamp(y-1, h) ] + px_bl = org[ self.clamp(x-1, w), self.clamp(y-1, h) ] + px_l = org[ self.clamp(x-1, w), self.clamp(y, h) ] + px_tl = org[ self.clamp(x-1, w), self.clamp(y+1, h) ] + + # Intensities of pixels + it_t = self.intensity(px_t) + it_tr = self.intensity(px_tr) + it_r = self.intensity(px_r) + it_br = self.intensity(px_br) + it_b = self.intensity(px_b) + it_bl = self.intensity(px_bl) + it_l = self.intensity(px_l) + it_tl = self.intensity(px_tl) + + # Sobel filter + dx = (it_tr + 2.0 * it_r + it_br) - (it_tl + 2.0 * it_l + it_bl) + dy = (it_bl + 2.0 * it_b + it_br) - (it_tl + 2.0 * it_t + it_tr) + dz = 10 # This is usually a good value for strength + v = (dx, dy, dz) + nrm = self.normalize_vector(v) + + # Invert height for our Orthos + if nrm[1] > 0: + nrm[1] = 0 - (abs(nrm[1])) + else: + nrm[1] = abs(nrm[1]) + + # Set pixel + nmp_pix[x,y] = (int(self.map_component(nrm[0])), int(self.map_component(nrm[1])), int(self.map_component(nrm[2])), alpha) + + mstr_msg("xp_normalmap", "[X-Plane] Normal map generated") + return nmp + + + # The funnction to call. Blends with the existing map, or creates a new one + def build_normalmap(self): + mstr_msg("xp_normalmap", "[X-Plane] Building normal map") + # The layer image + lyr = self.load_layer() + + # Make the normal map for the layer + nrm = self.generate_normal_map_for_layer(lyr) + + # Normal map final file name + nrmfln = mstr_datafolder + "Tiles/z_orthographic_" + self._latlngfld + "/normals/" + str(self._tv) + "_" + str(self._th) + ".png" + + # Check for existence of normal map file + ex = os.path.isfile(nrmfln) + + # Does not exist? Just save + if ex == False: + nrm.save(nrmfln) + + # Exists? Open it, composite both, save + if ex == True: + nrmmap = Image.open(nrmfln) + nrmmap.alpha_composite(nrm) + nrmmap.save(nrmfln) + mstr_msg("xp_normalmap", "[X-Plane] Normal map saved")