{"spec_id":"map-tile-background","library":"makie","language":"julia","code":"# anyplot.ai\n# map-tile-background: Map with Tile Background\n# Library: makie 0.22.10 | Julia 1.11.9\n# Quality: 88/100 | Created: 2026-05-27\n\nusing CairoMakie\nusing Colors\nusing Random\nusing Downloads\nusing PNGFiles\n\nRandom.seed!(42)\n\n# Theme tokens\nconst THEME       = get(ENV, \"ANYPLOT_THEME\", \"light\")\nconst PAGE_BG     = THEME == \"light\" ? colorant\"#FAF8F1\" : colorant\"#1A1A17\"\nconst INK         = THEME == \"light\" ? colorant\"#1A1A17\" : colorant\"#F0EFE8\"\nconst INK_SOFT    = THEME == \"light\" ? colorant\"#4A4A44\" : colorant\"#B8B7B0\"\nconst ANYPLOT_SEQ = cgrad([colorant\"#009E73\", colorant\"#4467A3\"])\n\n# Theme-adaptive tile providers\nconst TILE_URL   = THEME == \"light\" ?\n    \"https://tile.openstreetmap.org/{z}/{x}/{y}.png\" :\n    \"https://a.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png\"\nconst TILE_CREDIT = THEME == \"light\" ?\n    \"© OpenStreetMap contributors\" :\n    \"© OpenStreetMap contributors © CARTO\"\n\n# Web Mercator helpers (EPSG:3857 slippy-map tile convention)\nlat_to_merc(lat)     = log(tan(lat * π / 180) + 1 / cos(lat * π / 180))\nmerc_to_lat(m)       = atan(sinh(m)) * 180 / π\nlon_to_tx(lon, z)    = clamp(floor(Int, (lon + 180) / 360 * (1 << z)), 0, (1 << z) - 1)\nlat_to_ty(lat, z)    = clamp(floor(Int, (π - lat_to_merc(lat)) / (2π) * (1 << z)), 0, (1 << z) - 1)\ntx_lon_west(tx, z)   = tx / (1 << z) * 360 - 180\ntx_lon_east(tx, z)   = (tx + 1) / (1 << z) * 360 - 180\nty_merc_north(ty, z) = π * (1 - 2 * ty / (1 << z))\nty_merc_south(ty, z) = π * (1 - 2 * (ty + 1) / (1 << z))\n\n# City data: major tourist destinations with annual visitor counts (millions)\nconst city_names = [\n    \"Paris\", \"London\", \"New York\", \"Tokyo\", \"Barcelona\",\n    \"Rome\", \"Dubai\", \"Istanbul\", \"Amsterdam\", \"Singapore\",\n    \"Sydney\", \"Bangkok\", \"Mumbai\", \"Berlin\", \"Prague\",\n    \"Vienna\", \"Budapest\", \"Lisbon\", \"Copenhagen\", \"Stockholm\",\n]\nconst lons = Float64[\n    2.35, -0.12, -74.0, 139.7, 2.17,\n    12.5, 55.3, 28.98, 4.9, 103.8,\n    151.2, 100.5, 72.88, 13.41, 14.42,\n    16.37, 19.04, -9.14, 12.57, 18.07,\n]\nconst lats = Float64[\n    48.85, 51.51, 40.71, 35.68, 41.39,\n    41.9, 25.2, 41.01, 52.37, 1.35,\n    -33.87, 13.75, 19.08, 52.52, 50.08,\n    48.21, 47.5, 38.72, 55.68, 59.33,\n]\nconst visitors = Float64[\n    34.0, 31.0, 13.6, 12.9, 9.0,\n    9.7, 16.7, 14.0, 8.6, 19.1,\n    3.8, 22.8, 7.0, 5.3, 4.4,\n    6.8, 4.4, 4.1, 2.7, 2.9,\n]\n\n# Convert latitudes to Mercator Y for correct Web Mercator projection\ny_merc       = lat_to_merc.(lats)\nv_min        = minimum(visitors)\nv_max        = maximum(visitors)\nv_norm       = (visitors .- v_min) ./ (v_max - v_min)\nmarker_sizes = 10.0 .+ 28.0 .* v_norm\n\n# Padded axis extent (12 % padding in each direction)\nlon_pad      = (maximum(lons)   - minimum(lons))   * 0.12\nmerc_pad     = (maximum(y_merc) - minimum(y_merc)) * 0.12\nax_lon_min   = minimum(lons)   - lon_pad\nax_lon_max   = maximum(lons)   + lon_pad\nax_merc_min  = minimum(y_merc) - merc_pad\nax_merc_max  = maximum(y_merc) + merc_pad\npad_lat_north = merc_to_lat(ax_merc_max)\npad_lat_south = merc_to_lat(ax_merc_min)\n\n# Tile grid parameters\nconst ZOOM    = 3\nconst TILE_PX = 256\n\ntx_min = lon_to_tx(ax_lon_min, ZOOM)\ntx_max = lon_to_tx(ax_lon_max, ZOOM)\nty_min = lat_to_ty(pad_lat_north, ZOOM)   # north edge → smaller tile y index\nty_max = lat_to_ty(pad_lat_south, ZOOM)   # south edge → larger tile y index\n\nn_tx = tx_max - tx_min + 1\nn_ty = ty_max - ty_min + 1\n\n# Stitched raster: [pixel_x, pixel_y] where x = west→east, y = north→south\nstitched = fill(RGBA{Float32}(0.87f0, 0.87f0, 0.87f0, 1.0f0), TILE_PX * n_tx, TILE_PX * n_ty)\n\nmktempdir() do tmpdir\n    for iy in 0:(n_ty - 1), ix in 0:(n_tx - 1)\n        tx  = tx_min + ix\n        ty  = ty_min + iy\n        url = replace(TILE_URL,\n            \"{z}\" => string(ZOOM), \"{x}\" => string(tx), \"{y}\" => string(ty))\n        fpath = joinpath(tmpdir, \"$(tx)_$(ty).png\")\n        try\n            Downloads.download(url, fpath;\n                headers = [\"User-Agent\" =>\n                    \"anyplot.ai static visualization; contact@anyplot.ai\"])\n            tile_mat = PNGFiles.load(fpath)   # size = (height, width) = [row, col] = [y, x]\n            # Transpose to [x, y] layout expected by the stitched array\n            tile_t = permutedims(RGBA{Float32}.(tile_mat), (2, 1))\n            stitched[ix*TILE_PX+1:(ix+1)*TILE_PX, iy*TILE_PX+1:(iy+1)*TILE_PX] = tile_t\n        catch e\n            @warn \"Tile ($tx, $ty) unavailable\" exception = e\n        end\n    end\nend\n\n# Geographic bounds of the full stitched raster\nimg_lon_west   = tx_lon_west(tx_min, ZOOM)\nimg_lon_east   = tx_lon_east(tx_max, ZOOM)\nimg_merc_north = ty_merc_north(ty_min, ZOOM)\nimg_merc_south = ty_merc_south(ty_max, ZOOM)\n\n# Flip y so index 1 maps to the south (Makie image! convention: y increases upward)\nimg_makie = reverse(stitched, dims = 2)\n\n# Latitude tick marks in Mercator Y with degree labels\nlat_ticks_deg = filter(l -> pad_lat_south < l < pad_lat_north, [-30.0, 0.0, 30.0, 60.0])\nlat_tick_merc = lat_to_merc.(lat_ticks_deg)\nlat_tick_strs = [l >= 0 ? \"$(round(Int, l))°N\" : \"$(round(Int, -l))°S\" for l in lat_ticks_deg]\n\nlon_ticks_deg = filter(l -> ax_lon_min < l < ax_lon_max, [-60.0, 0.0, 60.0, 120.0])\nlon_tick_strs = [l >= 0 ? \"$(round(Int, l))°E\" : \"$(round(Int, -l))°W\" for l in lon_ticks_deg]\n\n# Figure\nfig = Figure(\n    size            = (1600, 900),\n    fontsize        = 14,\n    backgroundcolor = PAGE_BG,\n)\n\nax = Axis(\n    fig[1, 1];\n    title              = \"map-tile-background · julia · makie · anyplot.ai\",\n    titlesize          = 20,\n    titlecolor         = INK,\n    xlabel             = \"Longitude\",\n    ylabel             = \"Latitude\",\n    xlabelsize         = 14,\n    ylabelsize         = 14,\n    xlabelcolor        = INK,\n    ylabelcolor        = INK,\n    xticklabelsize     = 11,\n    yticklabelsize     = 11,\n    xticklabelcolor    = INK_SOFT,\n    yticklabelcolor    = INK_SOFT,\n    xtickcolor         = INK_SOFT,\n    ytickcolor         = INK_SOFT,\n    backgroundcolor    = PAGE_BG,\n    topspinevisible    = false,\n    rightspinevisible  = false,\n    leftspinecolor     = INK_SOFT,\n    bottomspinecolor   = INK_SOFT,\n    xgridvisible       = false,\n    ygridvisible       = false,\n    xticks             = (lon_ticks_deg, lon_tick_strs),\n    yticks             = (lat_tick_merc, lat_tick_strs),\n    limits             = (ax_lon_min, ax_lon_max, ax_merc_min, ax_merc_max),\n)\n\n# Tile background layer\nimage!(ax, (img_lon_west, img_lon_east), (img_merc_south, img_merc_north), img_makie)\n\n# Data layer: scatter by visitor count (size + color)\nscatter!(ax, lons, y_merc;\n    color       = v_norm,\n    colormap    = ANYPLOT_SEQ,\n    markersize  = marker_sizes,\n    strokewidth = 1.5,\n    strokecolor = PAGE_BG,\n)\n\n# Labels for top 8 most-visited cities\ntop8 = sortperm(visitors, rev = true)[1:8]\nfor i in top8\n    text!(ax, lons[i], y_merc[i];\n        text     = city_names[i],\n        fontsize = 10,\n        color    = INK,\n        offset   = (8, 5),\n        align    = (:left, :bottom),\n    )\nend\n\n# Tile provider attribution (bottom-right)\ntext!(ax, ax_lon_max, ax_merc_min;\n    text     = TILE_CREDIT,\n    fontsize = 9,\n    color    = INK_SOFT,\n    align    = (:right, :bottom),\n    offset   = (-4, 4),\n)\n\nColorbar(\n    fig[1, 2];\n    colormap       = ANYPLOT_SEQ,\n    limits         = (v_min, v_max),\n    label          = \"Annual Visitors (millions)\",\n    labelcolor     = INK,\n    tickcolor      = INK_SOFT,\n    ticklabelcolor = INK_SOFT,\n    width          = 16,\n)\n\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}