import geopandas as gpd
import matplotlib.pyplot as plt
import contextily as ctx
from matplotlib.ticker import FuncFormatter
import matplotlib.patches as patches
from pyproj import Transformer
import zipfile
import os
zip_file = "INAT_CIREMAI_DATA.zip"
if os.path.exists(zip_file):
with zipfile.ZipFile(zip_file, 'r') as zip_ref:
zip_ref.extractall(".")
shp_file = "INAT_CIREMAI_DATA.shp"
gdf = gpd.read_file(shp_file)
gdf_wm = gdf.to_crs(epsg=3857)
try:
wj_url = "https://raw.githubusercontent.com/superpikar/indonesia-geojson/master/indonesia-province-simple.json"
gdf_indo = gpd.read_file(wj_url)
gdf_jabar = gdf_indo[gdf_indo['Propinsi'] == 'JAWA BARAT'].copy()
gdf_jabar_wm = gdf_jabar.to_crs(epsg=3857)
except:
gdf_jabar_wm = None
transformer = Transformer.from_crs("epsg:3857", "epsg:4326", always_xy=True)
def dms_string(decimal_degree, is_lat=False):
is_positive = decimal_degree >= 0
dd = abs(decimal_degree)
degrees = int(dd)
minutes = int((dd - degrees) * 60)
seconds = (dd - degrees - minutes/60) * 3600
direction = "N" if is_lat and is_positive else "S" if is_lat else "E" if is_positive else "W"
return f"{degrees}°{minutes}'{seconds:.0f}\"{direction}"
def format_x_axis(x, pos):
lon, _ = transformer.transform(x, 0)
return dms_string(lon, is_lat=False)
def format_y_axis(y, pos):
_, lat = transformer.transform(0, y)
return dms_string(lat, is_lat=True)
def add_north_arrow(ax, x=0.95, y=0.95, size=0.05):
ar_w, ar_h = size / 2.0, size
p1 = patches.Polygon([[x, y+ar_h], [x-ar_w, y], [x, y-ar_h]], transform=ax.transAxes, fc='black', ec='black', zorder=10)
p2 = patches.Polygon([[x, y+ar_h], [x+ar_w, y], [x, y-ar_h]], transform=ax.transAxes, fc='white', ec='black', zorder=10)
ax.add_patch(p1); ax.add_patch(p2)
ax.text(x, y+ar_h+0.01, 'N', transform=ax.transAxes, ha='center', va='bottom', fontsize=14, fontweight='bold', zorder=10)
fig, ax = plt.subplots(figsize=(14, 14))
gdf_wm.plot(column='iconic_tax', ax=ax, legend=True, categorical=True, cmap='tab10', markersize=60, edgecolor='white', linewidth=0.8, zorder=5,
legend_kwds={'loc': 'lower left', 'title': 'Iconic Taxa', 'frameon':True, 'shadow':True})
ctx.add_basemap(ax, source=ctx.providers.OpenStreetMap.Mapnik)
ax.grid(True, linestyle='--', alpha=0.5, color='black', linewidth=0.5, zorder=2)
ax.xaxis.set_major_formatter(FuncFormatter(format_x_axis))
ax.yaxis.set_major_formatter(FuncFormatter(format_y_axis))
plt.xticks(fontsize=10); plt.yticks(fontsize=10, rotation=90, va='center')
ax.set_xlabel("Longitude", fontsize=11, labelpad=10)
ax.set_ylabel("Latitude", fontsize=11, labelpad=10)
ax.text(0.5, 1.06, "FLORA & FAUNA DISTRIBUTION MAP (INATURALIST)",
transform=ax.transAxes, ha='center', va='bottom',
fontsize=22, fontweight='bold', color='black', fontname='Arial')
ax.text(0.5, 1.02, "Study Area: Mount Ciremai National Park, West Java",
transform=ax.transAxes, ha='center', va='bottom',
fontsize=14, fontweight='normal', style='italic', color='#333333')
add_north_arrow(ax, x=0.92, y=0.90, size=0.06)
if gdf_jabar_wm is not None:
ax_inset = ax.inset_axes([0.68, 0.68, 0.3, 0.3])
gdf_jabar_wm.plot(ax=ax_inset, alpha=0)
ctx.add_basemap(ax_inset, source=ctx.providers.CartoDB.Positron, zoom=8)
gdf_jabar_wm.plot(ax=ax_inset, facecolor='none', edgecolor='#444444', linewidth=1, zorder=2)
main_xlim, main_ylim = ax.get_xlim(), ax.get_ylim()
rect = patches.Rectangle((main_xlim[0], main_ylim[0]), main_xlim[1]-main_xlim[0], main_ylim[1]-main_ylim[0],
linewidth=2, edgecolor='red', facecolor='none', zorder=5)
ax_inset.add_patch(rect)
ax_inset.set_title("Research Location", fontsize=10)
ax_inset.set_axis_off()
minx, miny, maxx, maxy = gdf_jabar_wm.total_bounds
ax_inset.set_xlim(minx - 20000, maxx + 20000)
ax_inset.set_ylim(miny - 20000, maxy + 20000)
plt.tight_layout()
plt.subplots_adjust(top=0.90)
plt.show()