I use the thunderforest API to plot points on a map. I figured out how to scale my longitude axis so that it fits the map, using this. Basically I know how width my map is in pixel and I know how many degree of longitude correspond to 256 pixel at each zoom level. With that I figure out how wide my map is in degrees of longitude.
My map is as high as it is wide in pixel (i.e. 1100x1100). So at a zoom level of 14, a width of 1100 pixel corresponds to 0.09453125° longitude. I now assume that a height of 1100 pixel would correspond to 0.09453125° latitude. But this is wrong. As you can see in this map the points should be a circle around the Launch point. But they are in more of an elliptical shape.
I found this on the openstreetmap website.
Values listed in the column "m / pixels" gives the number of meters per pixel at that zoom level for 256-pixel wide tiles. These values for "m / pixel" are calculated with an Earth radius of 6372.7982 km and hold at the Equator; for other latitudes the values must be multiplied by the cosine (approximately assuming a perfect spheric shape of the geoid) of the latitude.
But I couldn't figure what I needed to multiply by cos(latitude). Can someone help me to figure out what to do here?
My full code:
# data
launch_latitude = np.degrees(TOTAL_DATA[0]['TYPE_LATITUDE'][0][0])
launch_longitude = np.degrees(TOTAL_DATA[0]['TYPE_LONGITUDE'][0][0])
# variabels
zoom_level = 14 # 0 <= int <= 20
map_width_pixel = 1100 # max: 2560
save_fig = True
plot_name = 'name'
plot_title = 'NAME'
img_name = f'z{zoom_level}_w{map_width_pixel}_{launch_latitude}N_{launch_longitude}E.png'
img_path = f'../env/images/{img_name}'
tile_width_at_zoom_level = {0:360,1:180,2:90,3:45,4:22.5,5:11.25,6:5.625,7:2.813,8:1.406,9:0.703,10:0.352,11:0.176,12:0.088,13:0.044,14:0.022,15:0.011,16:0.005,17:0.003,18:0.001,19:0.0005,20:0.00025}
map_width_deg = tile_width_at_zoom_level[zoom_level]/256 * map_width_pixel
padding_long = map_width_deg/2
padding_lat = map_width_deg/2
BBox = [launch_longitude - padding_long, launch_longitude + padding_long, launch_latitude - padding_lat, launch_latitude + padding_lat]
# download map, if image doesn't allready exist.
if not os.path.isfile(img_path):
# get URL
zoom = zoom_level
style = 'Landscape'
apikey = '--- API KEY ---'
lon, lat = launch_longitude, launch_latitude
width, height = map_width_pixel, map_width_pixel
URL = f'https://tile.thunderforest.com/static/{style}/{lon},{lat},{zoom}/{width}x{height}.png?apikey={apikey}'
# save img
r = requests.get(URL, stream=True)
r.raw.decode_content = True
with open(img_path,'wb') as f:
shutil.copyfileobj(r.raw, f)
# plot
fig, ax = plt.subplots(figsize = (18,7))
map_img = plt.imread(img_path)
attribution = 'Maps © www.thunderforest.com, Data © www.osm.org/copyright'
ax.imshow(map_img, zorder=0, extent = BBox, aspect= 'equal')
ax.scatter(launch_longitude, launch_latitude, zorder=4, marker='^', color='black', label='Launch')
for zorder, raw_data, label in zip([1,2,3], TOTAL_DATA, ['Nominal Flight', 'No Main', 'No Parachute']):
land_latitude = [np.degrees(flight[-1]) for flight in raw_data['TYPE_LATITUDE']]
land_longitude = [np.degrees(flight[-1]) for flight in raw_data['TYPE_LONGITUDE']]
ax.scatter(land_longitude, land_latitude, zorder=zorder, marker='x', label=label)
ax.set_title(f'{plot_title} Dropzone Analysis')
ax.set_xlim(BBox[0],BBox[1])
ax.set_ylim(BBox[2],BBox[3])
ax.set_xlabel('Longitude [WGS84]')
ax.set_ylabel('Latitude [WGS84]')
ax.text(x=1, y=0.01, s=attribution, horizontalalignment='right', verticalalignment='bottom', rotation='vertical', fontsize=8, color='gray', alpha=0.9, transform=ax.transAxes)
ax.legend()
if save_fig:
plt.savefig(f'../results/{plot_name}_dropzone_analysis.pdf')
plt.show()