[OTTD JGRPP] SPAIN AND SOUTH OF FRANCE 8k (9k towns)

Post your custom scenarios here. Saved games also welcome. All Transport Tycoon games acceptable (including TTDPatch and OpenTTD).
Post Reply
jerby666
Engineer
Engineer
Posts: 18
Joined: 09 Mar 2023 18:12

[OTTD JGRPP] SPAIN AND SOUTH OF FRANCE 8k (9k towns)

Post by jerby666 »

Hi! We come from here: viewtopic.php?t=90654

For some time, I’ve wanted to represent Spain on an 8K map for OpenTTD, but when I tried it in the previous Project Iberia, I couldn’t achieve the results I wanted. I ran into the issue of having to manually place each city and town, which made the task extremely tedious. On top of that, once I started placing cities, the scenario became locked with whatever patches I had decided to apply.

That’s why I changed my approach to the problem. I created an 8K heightmap, and thanks to ChatGPT (I have no knowledge of programming), I developed some Python scripts that can automatically place the cities I input. Then, I run them through a second script that scales the population based on real population data.

One of the biggest challenges I faced was that the projection is so large that there’s a slight distortion. This made the calculations much more complex, leading to days of trial and error. In the end, I had to settle for using a projection that doesn’t include Portugal or part of Galicia, but even so, the result is impressive.

Of course, the process isn’t perfect, but maybe it can serve as a base for other users to develop an improved version. That’s why I’ll upload all the code so that those who know programming can use it and improve it if they want.

And well, the result is what you see—an automatically populated map of Spain, so everyone can create their own scenario and use whichever NewGRFs they prefer. Here are some screenshots:

Image

Image

Image







Script in python wich can place every city on their coordinates:
[+] Spoiler
#!/usr/bin/env python3
import json
import time
import random
from geopy.geocoders import Nominatim
from geopy.exc import GeocoderServiceError

# Constantes del mapa y parámetros de conversión
CENTER_LAT = 40.1452893 # Latitud del centro
CENTER_LON = -2.197265625 # Longitud del centro
CENTER_PIXEL = 4096 # Centro en un mapa de 8192x8192
RES_V = 0.001047 # Resolución vertical base (grados/px)
RES_H = 0.001373 # Resolución horizontal (grados/px)
CORR_FACTOR = 0.00767 # Factor de corrección en la conversión vertical
PIXEL_MAX = 8192 # Tamaño del mapa (para normalizar)

def lat_to_pixel_y(lat):
"""
Convierte la latitud a la coordenada vertical (en píxeles).
"""
denom = RES_V * (1 - CORR_FACTOR * (lat - CENTER_LAT))
offset = (lat - CENTER_LAT) / denom
pixel_y = CENTER_PIXEL - offset
return pixel_y

def lon_to_pixel_x(lon):
"""
Convierte la longitud a la coordenada horizontal (en píxeles).
"""
offset = (lon - CENTER_LON) / RES_H
pixel_x = CENTER_PIXEL + offset
return pixel_x

def compute_normalized_coordinates(lat, lon):
"""
Calcula las coordenadas normalizadas.
"""
pixel_x = lon_to_pixel_x(lon)
pixel_y = lat_to_pixel_y(lat)
norm_x = pixel_y / PIXEL_MAX # Nota: ¿Es correcto usar pixel_y para norm_x?
norm_y = pixel_x / PIXEL_MAX # Nota: ¿Es correcto usar pixel_x para norm_y?
return norm_x, norm_y

def is_valid_normalized_coords(norm_x, norm_y):
"""
Valida que las coordenadas normalizadas estén en el rango [0, 1].
"""
return 0.0 <= norm_x <= 1.0 and 0.0 <= norm_y <= 1.0

def get_city_coordinates(city_name, geolocator, max_retries=3):
"""
Obtiene las coordenadas de una ciudad con reintentos en caso de fallo.
"""
retries = 0
while retries < max_retries:
try:
location = geolocator.geocode(city_name, timeout=15)
if location:
return location.latitude, location.longitude
else:
print(f"Advertencia: No se encontró la ciudad '{city_name}'.")
return None, None
except GeocoderServiceError as e:
print(f"Error al obtener '{city_name}': {e}. Reintentando...")
retries += 1
time.sleep(2 + random.uniform(0, 2))
print(f"Error persistente con '{city_name}', omitiéndola.")
return None, None

def main():
cities = [
"Gibraltar"
]

geolocator = Nominatim(user_agent="city_mapper", timeout=10)
output = []

for city in cities:
lat, lon = get_city_coordinates(city, geolocator)
if lat is not None and lon is not None:
norm_x, norm_y = compute_normalized_coordinates(lat, lon)
if is_valid_normalized_coords(norm_x, norm_y):
output.append({
"name": city,
"population": 1000,
"city": True,
"x": norm_x,
"y": norm_y
})
else:
print(f"Advertencia: Coordenadas fuera de rango para '{city}'. Omitiendo.")
time.sleep(5 + random.uniform(0, 3))

with open("cities.json", "w", encoding="utf-8") as f:
json.dump(output, f, ensure_ascii=False, indent=4)

print(json.dumps(output, ensure_ascii=False, indent=4))

if __name__ == "__main__":
main()
Script in python that check the real population and scales it from 3 to 1000. If it is bigger than 500 it will set it as ''City''.
[+] Spoiler
import json
import time
import sys
from SPARQLWrapper import SPARQLWrapper, JSON

def get_population_from_wikidata(municipality_name):
"""
Consulta Wikidata para obtener la población (propiedad P1082) del municipio cuyo
rdfs:label en español coincida exactamente con municipality_name.
Devuelve la población como entero o None si no se encuentra.
"""
endpoint_url = "https://query.wikidata.org/sparql"
sparql = SPARQLWrapper(endpoint_url)
query = f"""
SELECT ?population WHERE {{
?municipality rdfs:label "{municipality_name}"@es.
?municipality wdt:P1082 ?population.
}} ORDER BY DESC(?population) LIMIT 1
"""
sparql.setQuery(query)
sparql.setReturnFormat(JSON)
try:
results = sparql.query().convert()
bindings = results["results"]["bindings"]
if bindings:
return int(bindings[0]["population"]["value"])
else:
return None
except Exception as e:
print(f"Error al consultar Wikidata para '{municipality_name}': {e}")
return None

def main(input_file, output_file):
# Cargar el JSON de entrada
with open(input_file, "r", encoding="utf-8") as f:
data = json.load(f)

fetched_populations = []
for item in data:
name = item["name"]
print(f"Buscando población para: {name}")
pop = get_population_from_wikidata(name)
if pop is None:
print(f" No se encontró población para '{name}', se usará el valor por defecto 1000.")
pop = 1000
else:
print(f" Población encontrada: {pop}")
# Guardamos la población real en un campo temporal (opcional)
item["real_population"] = pop
fetched_populations.append(pop)
# Pausa para evitar saturar el endpoint
time.sleep(1)

# Determinar el mínimo y máximo de la población real obtenida
min_pop = min(fetched_populations)
max_pop = max(fetched_populations)
print(f"\nPoblación mínima: {min_pop} / máxima: {max_pop}")

def scale_population(real_value):
if max_pop == min_pop:
return 1000
# Transformación lineal para mapear de [min_pop, max_pop] a [3, 1000]
scaled = 3 + ((real_value - min_pop) * 997) / (max_pop - min_pop)
return round(scaled)

# Actualizar cada municipio en la lista
for item in data:
real_value = item["real_population"]
scaled_value = scale_population(real_value)
item["population"] = scaled_value
item["city"] = True if scaled_value > 500 else False
# Opcional: eliminar el campo temporal
del item["real_population"]

# Guardar el resultado en un archivo de salida
with open(output_file, "w", encoding="utf-8") as f:
json.dump(data, f, indent=4, ensure_ascii=False)

print(f"\nProceso completado. Archivo generado: {output_file}")

if __name__ == "__main__":
if len(sys.argv) != 3:
print("Uso: python transform.py input.json output.json")
else:
main(sys.argv[1], sys.argv[2])

FILES. Heighmap, scenario, .json and scripts.
[+] Spoiler
https://www.transfernow.net/dl/20250226f4G3Dgup
https://limewire.com/d/0ada0c00-5f97-4d ... fcZ90KsHcQ





screenshot#42.png
(723.01 KiB) Not downloaded yet
screenshot#43.png
(638.35 KiB) Not downloaded yet
screenshot#44.png
(728.47 KiB) Not downloaded yet





DOWNLOADS on next post.
Last edited by jerby666 on 26 Feb 2025 09:30, edited 5 times in total.
jerby666
Engineer
Engineer
Posts: 18
Joined: 09 Mar 2023 18:12

Re: [OTTD JGRPP] SPAIN AND SOUTH OF FRANCE 8k (7k towns)

Post by jerby666 »

DOWNLOAD SCENARIO.

Image

Image



(Trying to figure out how to upload larger files)
Attachments
screenshot#48.png
(2.8 MiB) Not downloaded yet
screenshot#47.png
(861.47 KiB) Not downloaded yet
SPAIN AND SOUTH OF FRANCE.scn
(12.33 MiB) Downloaded 88 times
Last edited by jerby666 on 25 Feb 2025 15:07, edited 2 times in total.
ebla71
Route Supervisor
Route Supervisor
Posts: 498
Joined: 14 Apr 2021 21:48
Location: Earth

Re: [OTTD JGRPP] SPAIN AND SOUTH OF FRANCE 8k (7k towns)

Post by ebla71 »

jerby666 wrote: 24 Feb 2025 11:30 For some time, I’ve wanted to represent Spain on an 8K map for OpenTTD, but when I tried it in the previous Project Iberia, I couldn’t achieve the results I wanted. I ran into the issue of having to manually place each city and town, which made the task extremely tedious. On top of that, once I started placing cities, the scenario became locked with whatever patches I had decided to apply.

That’s why I changed my approach to the problem. I created an 8K heightmap, and thanks to ChatGPT (I have no knowledge of programming), I developed some Python scripts that can automatically place the cities I input. Then, I run them through a second script that scales the population based on real population data.

One of the biggest challenges I faced was that the projection is so large that there’s a slight distortion. This made the calculations much more complex, leading to days of trial and error. In the end, I had to settle for using a projection that doesn’t include Portugal or part of Galicia, but even so, the result is impressive.

Of course, the process isn’t perfect, but maybe it can serve as a base for other users to develop an improved version. That’s why I’ll upload all the code so that those who know programming can use it and improve it if they want.
I absolutely love the Python scripts!!!! :D

This makes use of the new function to build cities based on an external file, right?

I'll definitely give that a try while working on my 16k Germany+neighbours scenario where I will face a similar issue. However, currently still fixing the coastline siince the North Sea and Baltic Sea coasts are very close to sea level and don't import very well.
jerby666 wrote: 24 Feb 2025 11:35 Trying to figure out how to upload larger files
I think there is a fixed 25 MB limit to the forum since I also ran into that problem with my latest 16k scenario which did not upload at 26.6 MB and cannot be further compressed.
jerby666
Engineer
Engineer
Posts: 18
Joined: 09 Mar 2023 18:12

Re: [OTTD JGRPP] SPAIN AND SOUTH OF FRANCE 8k (7k towns)

Post by jerby666 »

ebla71 wrote: 25 Feb 2025 05:10

I absolutely love the Python scripts!!!! :D

This makes use of the new function to build cities based on an external file, right?

I'll definitely give that a try while working on my 16k Germany+neighbours scenario where I will face a similar issue. However, currently still fixing the coastline siince the North Sea and Baltic Sea coasts are very close to sea level and don't import very well.


I think there is a fixed 25 MB limit to the forum since I also ran into that problem with my latest 16k scenario which did not upload at 26.6 MB and cannot be further compressed.
Thank you! It took a lot of time of prompting with Chatgpt and Deepseek, but now it works! It is pretty accurate actually.

And yes, it generates a .json that you have to import while ingame on scenario editor.



Well, if I can't upload it here I'll try to upload it in other site so anyone will be able to download it. I'm just waiting the mod's answer because I don't know if it is against forum rules. Anyways I'll upload a vanilla scenario so you can test it and play it.
Post Reply

Return to “Scenarios and Saved Games”

Who is online

Users browsing this forum: No registered users and 2 guests