diff --git a/frontend/.tmp-style.json b/frontend/.tmp-style.json new file mode 100644 index 0000000..8dda233 --- /dev/null +++ b/frontend/.tmp-style.json @@ -0,0 +1,2065 @@ +{ + "version": 8, + "name": "Dark Matter", + "metadata": { + "mapbox:autocomposite": false, + "mapbox:groups": { + "101da9f13b64a08fa4b6ac1168e89e5f": { + "collapsed": false, + "name": "Places" + }, + "a14c9607bc7954ba1df7205bf660433f": { + "name": "Boundaries" + }, + "b6371a3f2f5a9932464fa3867530a2e5": { + "collapsed": false, + "name": "Transportation" + } + }, + "mapbox:type": "template", + "openmaptiles:mapbox:owner": "openmaptiles", + "openmaptiles:mapbox:source:url": "mapbox://openmaptiles.4qljc88t", + "openmaptiles:version": "3.x" + }, + "sources": { + "openmaptiles": { + "type": "vector", + "url": "mbtiles://{v3}" + } + }, + "sprite": "{styleJsonFolder}/sprite", + "glyphs": "{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "rgb(12,12,12)" + } + }, + { + "id": "water", + "type": "fill", + "source": "openmaptiles", + "source-layer": "water", + "filter": [ + "all", + [ + "==", + "$type", + "Polygon" + ], + [ + "!=", + "brunnel", + "tunnel" + ] + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-antialias": false, + "fill-color": "rgb(27 ,27 ,29)" + } + }, + { + "id": "landcover_ice_shelf", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landcover", + "maxzoom": 8, + "filter": [ + "all", + [ + "==", + "$type", + "Polygon" + ], + [ + "==", + "subclass", + "ice_shelf" + ] + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-color": "rgb(12,12,12)", + "fill-opacity": 0.7 + } + }, + { + "id": "landcover_glacier", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landcover", + "maxzoom": 8, + "filter": [ + "all", + [ + "==", + "$type", + "Polygon" + ], + [ + "==", + "subclass", + "glacier" + ] + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-color": "hsl(0, 1%, 2%)", + "fill-opacity": { + "base": 1, + "stops": [ + [ + 0, + 1 + ], + [ + 8, + 0.5 + ] + ] + } + } + }, + { + "id": "landuse_residential", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landuse", + "maxzoom": 9, + "filter": [ + "all", + [ + "==", + "$type", + "Polygon" + ], + [ + "==", + "class", + "residential" + ] + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-color": "hsl(0, 2%, 5%)", + "fill-opacity": 0.4 + } + }, + { + "id": "landcover_wood", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landcover", + "minzoom": 10, + "filter": [ + "all", + [ + "==", + "$type", + "Polygon" + ], + [ + "==", + "class", + "wood" + ] + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-color": "rgb(32,32,32)", + "fill-opacity": { + "base": 0.3, + "stops": [ + [ + 8, + 0 + ], + [ + 10, + 0.8 + ], + [ + 13, + 0.4 + ] + ] + }, + "fill-pattern": "wood-pattern", + "fill-translate": [ + 0, + 0 + ] + } + }, + { + "id": "landuse_park", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landuse", + "filter": [ + "all", + [ + "==", + "$type", + "Polygon" + ], + [ + "==", + "class", + "park" + ] + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-color": "rgb(32,32,32)" + } + }, + { + "id": "waterway", + "type": "line", + "source": "openmaptiles", + "source-layer": "waterway", + "filter": [ + "==", + "$type", + "LineString" + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "line-color": "rgb(27 ,27 ,29)" + } + }, + { + "id": "water_name", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "water_name", + "filter": [ + "==", + "$type", + "LineString" + ], + "layout": { + "symbol-placement": "line", + "symbol-spacing": 500, + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": [ + "Metropolis Medium Italic", + "Noto Sans Italic" + ], + "text-rotation-alignment": "map", + "text-size": 12 + }, + "paint": { + "text-color": "hsla(0, 0%, 0%, 0.7)", + "text-halo-color": "hsl(0, 0%, 27%)" + } + }, + { + "id": "building", + "type": "fill", + "source": "openmaptiles", + "source-layer": "building", + "minzoom": 12, + "filter": [ + "==", + "$type", + "Polygon" + ], + "paint": { + "fill-antialias": true, + "fill-color": "rgb(10,10,10)", + "fill-outline-color": "rgb(27 ,27 ,29)" + } + }, + { + "id": "aeroway-taxiway", + "type": "line", + "metadata": { + "mapbox:group": "1444849345966.4436" + }, + "source": "openmaptiles", + "source-layer": "aeroway", + "minzoom": 12, + "filter": [ + "all", + [ + "in", + "class", + "taxiway" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "#181818", + "line-opacity": 1, + "line-width": { + "base": 1.55, + "stops": [ + [ + 13, + 1.8 + ], + [ + 20, + 20 + ] + ] + } + } + }, + { + "id": "aeroway-runway-casing", + "type": "line", + "metadata": { + "mapbox:group": "1444849345966.4436" + }, + "source": "openmaptiles", + "source-layer": "aeroway", + "minzoom": 11, + "filter": [ + "all", + [ + "in", + "class", + "runway" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "rgba(60,60,60,0.8)", + "line-opacity": 1, + "line-width": { + "base": 1.5, + "stops": [ + [ + 11, + 5 + ], + [ + 17, + 55 + ] + ] + } + } + }, + { + "id": "aeroway-area", + "type": "fill", + "metadata": { + "mapbox:group": "1444849345966.4436" + }, + "source": "openmaptiles", + "source-layer": "aeroway", + "minzoom": 4, + "filter": [ + "all", + [ + "==", + "$type", + "Polygon" + ], + [ + "in", + "class", + "runway", + "taxiway" + ] + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-color": "#000", + "fill-opacity": 1 + } + }, + { + "id": "aeroway-runway", + "type": "line", + "metadata": { + "mapbox:group": "1444849345966.4436" + }, + "source": "openmaptiles", + "source-layer": "aeroway", + "minzoom": 11, + "filter": [ + "all", + [ + "in", + "class", + "runway" + ], + [ + "==", + "$type", + "LineString" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "#000", + "line-opacity": 1, + "line-width": { + "base": 1.5, + "stops": [ + [ + 11, + 4 + ], + [ + 17, + 50 + ] + ] + } + } + }, + { + "id": "road_area_pier", + "type": "fill", + "metadata": {}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "==", + "$type", + "Polygon" + ], + [ + "==", + "class", + "pier" + ] + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-antialias": true, + "fill-color": "rgb(12,12,12)" + } + }, + { + "id": "road_pier", + "type": "line", + "metadata": {}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "in", + "class", + "pier" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-color": "rgb(12,12,12)", + "line-width": { + "base": 1.2, + "stops": [ + [ + 15, + 1 + ], + [ + 17, + 4 + ] + ] + } + } + }, + { + "id": "highway_path", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "==", + "class", + "path" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "rgb(27 ,27 ,29)", + "line-dasharray": [ + 1.5, + 1.5 + ], + "line-opacity": 0.9, + "line-width": { + "base": 1.2, + "stops": [ + [ + 13, + 1 + ], + [ + 20, + 10 + ] + ] + } + } + }, + { + "id": "highway_minor", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 8, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "in", + "class", + "minor", + "service", + "track" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "#181818", + "line-opacity": 0.9, + "line-width": { + "base": 1.55, + "stops": [ + [ + 13, + 1.8 + ], + [ + 20, + 20 + ] + ] + } + } + }, + { + "id": "highway_major_casing", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 11, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "in", + "class", + "primary", + "secondary", + "tertiary", + "trunk" + ] + ], + "layout": { + "line-cap": "butt", + "line-join": "miter", + "visibility": "visible" + }, + "paint": { + "line-color": "rgba(60,60,60,0.8)", + "line-dasharray": [ + 12, + 0 + ], + "line-width": { + "base": 1.3, + "stops": [ + [ + 10, + 3 + ], + [ + 20, + 23 + ] + ] + } + } + }, + { + "id": "highway_major_inner", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 11, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "in", + "class", + "primary", + "secondary", + "tertiary", + "trunk" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "hsl(0, 0%, 7%)", + "line-width": { + "base": 1.3, + "stops": [ + [ + 10, + 2 + ], + [ + 20, + 20 + ] + ] + } + } + }, + { + "id": "highway_major_subtle", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 6, + "maxzoom": 11, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "in", + "class", + "primary", + "secondary", + "tertiary", + "trunk" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "#2a2a2a", + "line-width": { + "stops": [ + [ + 6, + 0 + ], + [ + 8, + 2 + ] + ] + } + } + }, + { + "id": "highway_motorway_casing", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 6, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "==", + "class", + "motorway" + ] + ], + "layout": { + "line-cap": "butt", + "line-join": "miter", + "visibility": "visible" + }, + "paint": { + "line-color": "rgba(60,60,60,0.8)", + "line-dasharray": [ + 2, + 0 + ], + "line-opacity": 1, + "line-width": { + "base": 1.4, + "stops": [ + [ + 5.8, + 0 + ], + [ + 6, + 3 + ], + [ + 20, + 40 + ] + ] + } + } + }, + { + "id": "highway_motorway_inner", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 6, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "==", + "class", + "motorway" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": { + "base": 1, + "stops": [ + [ + 5.8, + "hsla(0, 0%, 85%, 0.53)" + ], + [ + 6, + "#000" + ] + ] + }, + "line-width": { + "base": 1.4, + "stops": [ + [ + 4, + 2 + ], + [ + 6, + 1.3 + ], + [ + 20, + 30 + ] + ] + } + } + }, + { + "id": "road_oneway", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 15, + "filter": [ + "all", + [ + "==", + "oneway", + 1 + ] + ], + "layout": { + "symbol-placement": "line", + "icon-image": "oneway", + "symbol-spacing": 200, + "icon-padding": 2, + "icon-rotation-alignment": "map", + "icon-rotate": 0, + "icon-size": { + "stops": [ + [ + 15, + 0.5 + ], + [ + 19, + 1 + ] + ] + } + }, + "paint": { + "icon-opacity": 0.5 + } + }, + { + "id": "road_oneway_opposite", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 15, + "filter": [ + "all", + [ + "==", + "oneway", + -1 + ] + ], + "layout": { + "symbol-placement": "line", + "icon-image": "oneway", + "symbol-spacing": 200, + "icon-padding": 2, + "icon-rotation-alignment": "map", + "icon-rotate": 180, + "icon-size": { + "stops": [ + [ + 15, + 0.5 + ], + [ + 19, + 1 + ] + ] + } + }, + "paint": { + "icon-opacity": 0.5 + } + }, + { + "id": "highway_motorway_subtle", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "maxzoom": 6, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "==", + "class", + "motorway" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "#181818", + "line-width": { + "base": 1.4, + "stops": [ + [ + 4, + 2 + ], + [ + 6, + 1.3 + ] + ] + } + } + }, + { + "id": "railway_transit", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 16, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "all", + [ + "==", + "class", + "transit" + ], + [ + "!in", + "brunnel", + "tunnel" + ] + ] + ], + "layout": { + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "rgb(35,35,35)", + "line-width": 3 + } + }, + { + "id": "railway_transit_dashline", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 16, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "all", + [ + "==", + "class", + "transit" + ], + [ + "!in", + "brunnel", + "tunnel" + ] + ] + ], + "layout": { + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "rgb(12,12,12)", + "line-dasharray": [ + 3, + 3 + ], + "line-width": 2 + } + }, + { + "id": "railway_minor", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 16, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "all", + [ + "==", + "class", + "rail" + ], + [ + "has", + "service" + ] + ] + ], + "layout": { + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "rgb(35,35,35)", + "line-width": 3 + } + }, + { + "id": "railway_minor_dashline", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 16, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "all", + [ + "==", + "class", + "rail" + ], + [ + "has", + "service" + ] + ] + ], + "layout": { + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "rgb(12,12,12)", + "line-dasharray": [ + 3, + 3 + ], + "line-width": 2 + } + }, + { + "id": "railway", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 13, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "==", + "class", + "rail" + ], + [ + "!has", + "service" + ] + ], + "layout": { + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "rgb(35,35,35)", + "line-width": { + "base": 1.3, + "stops": [ + [ + 16, + 3 + ], + [ + 20, + 7 + ] + ] + } + } + }, + { + "id": "railway_dashline", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 13, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "==", + "class", + "rail" + ], + [ + "!has", + "service" + ] + ], + "layout": { + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "rgb(12,12,12)", + "line-dasharray": [ + 3, + 3 + ], + "line-width": { + "base": 1.3, + "stops": [ + [ + 16, + 2 + ], + [ + 20, + 6 + ] + ] + } + } + }, + { + "id": "highway_name_other", + "type": "symbol", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation_name", + "filter": [ + "all", + [ + "!=", + "class", + "motorway" + ], + [ + "==", + "$type", + "LineString" + ] + ], + "layout": { + "symbol-placement": "line", + "symbol-spacing": 350, + "text-field": "{name:latin} {name:nonlatin}", + "text-font": [ + "Metropolis Regular", + "Noto Sans Regular" + ], + "text-max-angle": 30, + "text-pitch-alignment": "viewport", + "text-rotation-alignment": "map", + "text-size": 10, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "text-color": "rgba(80, 78, 78, 1)", + "text-halo-blur": 0, + "text-halo-color": "rgba(0, 0, 0, 1)", + "text-halo-width": 1, + "text-translate": [ + 0, + 0 + ] + } + }, + { + "id": "highway_name_motorway", + "type": "symbol", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation_name", + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "==", + "class", + "motorway" + ] + ], + "layout": { + "symbol-placement": "line", + "symbol-spacing": 350, + "text-field": "{ref}", + "text-font": [ + "Metropolis Light", + "Noto Sans Regular" + ], + "text-pitch-alignment": "viewport", + "text-rotation-alignment": "viewport", + "text-size": 10, + "visibility": "visible" + }, + "paint": { + "text-color": "hsl(0, 0%, 37%)", + "text-translate": [ + 0, + 2 + ] + } + }, + { + "id": "boundary_state", + "type": "line", + "metadata": { + "mapbox:group": "a14c9607bc7954ba1df7205bf660433f" + }, + "source": "openmaptiles", + "source-layer": "boundary", + "filter": [ + "==", + "admin_level", + 4 + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-blur": 0.4, + "line-color": "hsl(0, 0%, 21%)", + "line-dasharray": [ + 2, + 2 + ], + "line-opacity": 1, + "line-width": { + "base": 1.3, + "stops": [ + [ + 3, + 1 + ], + [ + 22, + 15 + ] + ] + } + } + }, + { + "id": "boundary_country_z0-4", + "type": "line", + "metadata": { + "mapbox:group": "a14c9607bc7954ba1df7205bf660433f" + }, + "source": "openmaptiles", + "source-layer": "boundary", + "maxzoom": 5, + "filter": [ + "all", + [ + "==", + "admin_level", + 2 + ], + [ + "!has", + "claimed_by" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-blur": { + "base": 1, + "stops": [ + [ + 0, + 0.4 + ], + [ + 22, + 4 + ] + ] + }, + "line-color": "hsl(0, 0%, 23%)", + "line-opacity": 1, + "line-width": { + "base": 1.1, + "stops": [ + [ + 3, + 1 + ], + [ + 22, + 20 + ] + ] + } + } + }, + { + "id": "boundary_country_z5-", + "type": "line", + "metadata": { + "mapbox:group": "a14c9607bc7954ba1df7205bf660433f" + }, + "source": "openmaptiles", + "source-layer": "boundary", + "minzoom": 5, + "filter": [ + "==", + "admin_level", + 2 + ], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-blur": { + "base": 1, + "stops": [ + [ + 0, + 0.4 + ], + [ + 22, + 4 + ] + ] + }, + "line-color": "hsl(0, 0%, 23%)", + "line-opacity": 1, + "line-width": { + "base": 1.1, + "stops": [ + [ + 3, + 1 + ], + [ + 22, + 20 + ] + ] + } + } + }, + { + "id": "place_other", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 14, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "in", + "class", + "hamlet", + "isolated_dwelling", + "neighbourhood" + ] + ], + "layout": { + "text-anchor": "center", + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": [ + "Metropolis Regular", + "Noto Sans Regular" + ], + "text-justify": "center", + "text-offset": [ + 0.5, + 0 + ], + "text-size": 10, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "text-color": "rgb(101,101,101)", + "text-halo-blur": 1, + "text-halo-color": "rgba(0,0,0,0.7)", + "text-halo-width": 1 + } + }, + { + "id": "place_suburb", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 15, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "==", + "class", + "suburb" + ] + ], + "layout": { + "text-anchor": "center", + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": [ + "Metropolis Regular", + "Noto Sans Regular" + ], + "text-justify": "center", + "text-offset": [ + 0.5, + 0 + ], + "text-size": 10, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "text-color": "rgb(101,101,101)", + "text-halo-blur": 1, + "text-halo-color": "rgba(0,0,0,0.7)", + "text-halo-width": 1 + } + }, + { + "id": "place_village", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 14, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "==", + "class", + "village" + ] + ], + "layout": { + "icon-size": 0.4, + "text-anchor": "left", + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": [ + "Metropolis Regular", + "Noto Sans Regular" + ], + "text-justify": "left", + "text-offset": [ + 0.5, + 0.2 + ], + "text-size": 10, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "icon-opacity": 0.7, + "text-color": "rgb(101,101,101)", + "text-halo-blur": 1, + "text-halo-color": "rgba(0,0,0,0.7)", + "text-halo-width": 1 + } + }, + { + "id": "place_town", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 15, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "==", + "class", + "town" + ] + ], + "layout": { + "icon-image": { + "base": 1, + "stops": [ + [ + 0, + "circle-11" + ], + [ + 9, + "" + ] + ] + }, + "icon-size": 0.4, + "text-anchor": { + "base": 1, + "stops": [ + [ + 0, + "left" + ], + [ + 8, + "center" + ] + ] + }, + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": [ + "Metropolis Regular", + "Noto Sans Regular" + ], + "text-justify": "left", + "text-offset": [ + 0.5, + 0.2 + ], + "text-size": 10, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "icon-opacity": 0.7, + "text-color": "rgb(101,101,101)", + "text-halo-blur": 1, + "text-halo-color": "rgba(0,0,0,0.7)", + "text-halo-width": 1 + } + }, + { + "id": "place_city", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 14, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "==", + "class", + "city" + ], + [ + ">", + "rank", + 3 + ] + ], + "layout": { + "icon-image": { + "base": 1, + "stops": [ + [ + 0, + "circle-11" + ], + [ + 9, + "" + ] + ] + }, + "icon-size": 0.4, + "text-anchor": { + "base": 1, + "stops": [ + [ + 0, + "left" + ], + [ + 8, + "center" + ] + ] + }, + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": [ + "Metropolis Regular", + "Noto Sans Regular" + ], + "text-justify": "left", + "text-offset": [ + 0.5, + 0.2 + ], + "text-size": 10, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "icon-opacity": 0.7, + "text-color": "rgb(101,101,101)", + "text-halo-blur": 1, + "text-halo-color": "rgba(0,0,0,0.7)", + "text-halo-width": 1 + } + }, + { + "id": "place_city_large", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 12, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "<=", + "rank", + 3 + ], + [ + "==", + "class", + "city" + ] + ], + "layout": { + "icon-image": { + "base": 1, + "stops": [ + [ + 0, + "circle-11" + ], + [ + 9, + "" + ] + ] + }, + "icon-size": 0.4, + "text-anchor": { + "base": 1, + "stops": [ + [ + 0, + "left" + ], + [ + 8, + "center" + ] + ] + }, + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": [ + "Metropolis Regular", + "Noto Sans Regular" + ], + "text-justify": "left", + "text-offset": [ + 0.5, + 0.2 + ], + "text-size": 14, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "icon-opacity": 0.7, + "text-color": "rgb(101,101,101)", + "text-halo-blur": 1, + "text-halo-color": "rgba(0,0,0,0.7)", + "text-halo-width": 1 + } + }, + { + "id": "place_state", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 12, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "==", + "class", + "state" + ] + ], + "layout": { + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": [ + "Metropolis Regular", + "Noto Sans Regular" + ], + "text-size": 10, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "text-color": "rgb(101,101,101)", + "text-halo-blur": 1, + "text-halo-color": "rgba(0,0,0,0.7)", + "text-halo-width": 1 + } + }, + { + "id": "place_country_other", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 8, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "==", + "class", + "country" + ], + [ + "!has", + "iso_a2" + ] + ], + "layout": { + "text-field": "{name:latin}", + "text-font": [ + "Metropolis Light Italic", + "Noto Sans Italic" + ], + "text-size": { + "base": 1, + "stops": [ + [ + 0, + 9 + ], + [ + 1, + 11 + ] + ] + }, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "text-color": "rgb(101,101,101)", + "text-halo-color": "rgba(0,0,0,0.7)", + "text-halo-width": 1.4 + } + }, + { + "id": "place_country_minor", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 8, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "==", + "class", + "country" + ], + [ + ">=", + "rank", + 2 + ], + [ + "has", + "iso_a2" + ] + ], + "layout": { + "text-field": "{name:latin}", + "text-font": [ + "Metropolis Regular", + "Noto Sans Regular" + ], + "text-size": { + "base": 1, + "stops": [ + [ + 0, + 10 + ], + [ + 6, + 12 + ] + ] + }, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "text-color": "rgb(101,101,101)", + "text-halo-color": "rgba(0,0,0,0.7)", + "text-halo-width": 1.4 + } + }, + { + "id": "place_country_major", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 6, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "<=", + "rank", + 1 + ], + [ + "==", + "class", + "country" + ], + [ + "has", + "iso_a2" + ] + ], + "layout": { + "text-anchor": "center", + "text-field": "{name:latin}", + "text-font": [ + "Metropolis Regular", + "Noto Sans Regular" + ], + "text-size": { + "base": 1.4, + "stops": [ + [ + 0, + 10 + ], + [ + 3, + 12 + ], + [ + 4, + 14 + ] + ] + }, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "text-color": "rgb(101,101,101)", + "text-halo-color": "rgba(0,0,0,0.7)", + "text-halo-width": 1.4 + } + } + ] +} \ No newline at end of file diff --git a/frontend/README.md b/frontend/README.md index d5e5a4c..dde6cb1 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -9,6 +9,12 @@ The **Pinar** font is installed and served locally. The application no longer re **Smart Bidi Typography:** To maintain perfect visual weight and baseline alignment when mixing English words within Persian text, we utilize a dynamic CSS calculation for Latin characters: `calc(1em - 2px)`. This ensures that English text automatically scales down slightly to match the natural density and baseline of the Persian font, preventing any visual imbalance in mixed-language sentences. ## Recent UI/UX Improvements +* **Projects Map Restoration**: Successfully re-integrated and mounted the interactive project overview map directly at the top of the Projects page to display all project markers. +* **Project Details Update**: Revamped and updated the presentation of project details for better clarity and structure. +* **Stats Section Relocation**: Moved the statistics (metrics) section higher up in the page layout for better visibility and impact. +* **FAQ Section Redesign**: Refactored the FAQ layout into a centered vertical stack with full RTL/LTR responsiveness, featuring an integrated centered header block (title, description, and accent line) and direction-aware (`text-start`) accordion content. +* **Newsletter Section Polish**: Updated the newsletter container to a clean solid `bg-copper` background while introducing high-contrast button styling (`bg-ink` default transitioning to white on hover) preserving site-wide kinetic animation logic. +* **Article Cards Spacing**: Refined vertical layout spacing and responsive gaps between title, excerpt, and footer blocks without impacting typography scales. * **Tiptap Editor Fixes**: * Resolved the initialization bug where the editor body remained empty during edit mode. * Fixed Tiptap block boundaries so formatting a new line (e.g., changing it to H2) no longer alters the formatting of the previous line. @@ -60,3 +66,11 @@ npm run build * `src/admin/` — Live editing layer (Digital Twin): includes the admin toolbar, entity drawers, and the article editor. * `src/seo.ts` — Manages page-level SEO tags (Title, Description, Open Graph, Canonical, JSON-LD). * `src/data/site.ts` — Shared TypeScript data models and interfaces. + +``` + +حالا اگر بخواهید این تغییرات جدید را هم در پیام کامیت خود بیاورید، می‌توانید از این کامیت یک‌خطی استفاده کنید: + +```bash +git commit -m "docs: update README with project details revamp, stats section relocation, FAQ layout, and map restoration" + diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 195dfd0..6ff7318 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -17,9 +17,12 @@ "@tiptap/static-renderer": "^3.31.3", "framer-motion": "^11.5.4", "lucide-react": "0.522.0", + "maplibre-gl": "^6.10.0", + "pmtiles": "^4.5.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-hook-form": "^7.88.0", + "react-icons": "^5.7.0", "react-multi-date-picker": "^4.5.2", "react-router-dom": "^6.26.2", "react-select": "^5.10.2", @@ -1041,6 +1044,92 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mapbox/jsonlint-lines-primitives": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.3.tgz", + "integrity": "sha512-0SElaV0uMxEnxzBhhX9WTuPyUeMsAN/SS0i16tjuba4/mio63MG9khjC1a0JAiPGXAwvwm4UfHJURCN7nyudQg==", + "license": "MIT", + "engines": { + "node": ">= 22" + } + }, + "node_modules/@mapbox/point-geometry": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-1.1.0.tgz", + "integrity": "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==", + "license": "ISC" + }, + "node_modules/@mapbox/tiny-sdf": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.2.0.tgz", + "integrity": "sha512-LVL4wgI9YAum5V+LNVQO6QgFBPw7/MIIY4XJPNsPDMrjEwcE+JfKk1LuIl8GnF197ejVdC9QdPaxrx5gfgdGXg==", + "license": "BSD-2-Clause" + }, + "node_modules/@mapbox/unitbezier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-1.0.0.tgz", + "integrity": "sha512-fqd515fjBmANKGGsQ286E2Wvj/XvDFpGzwJxq4CI6jMQue6Oy04uCKp+JWKF00xRTmk6cEu1jPJ9p3xqH8YWqQ==", + "license": "BSD-2-Clause" + }, + "node_modules/@mapbox/vector-tile": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-3.0.0.tgz", + "integrity": "sha512-Qf10S1uIHMk20ri/IVBnpS+esUEkVaR5Hftmz88jTInrpmWgPGJfPe3LVjjlE77trLx8tH6qjTG7uWH9hIq/0Q==", + "license": "BSD-3-Clause", + "dependencies": { + "@mapbox/point-geometry": "~1.1.0", + "@types/geojson": "^7946.0.16", + "pbf": "^5.0.0" + } + }, + "node_modules/@maplibre/geojson-vt": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@maplibre/geojson-vt/-/geojson-vt-6.1.1.tgz", + "integrity": "sha512-FVMOcmSP/yqol45t7StApEyTL5/vmqBCuFhH9n+fFuINenhaX+YgHHIt1yJ86S8kln3uJLcMvmEU2cfn6E2eCQ==", + "license": "ISC", + "dependencies": { + "kdbush": "^4.1.0" + } + }, + "node_modules/@maplibre/maplibre-gl-style-spec": { + "version": "26.4.4", + "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-26.4.4.tgz", + "integrity": "sha512-hG4RZR4PBd/+6A3ODdZ3AnQRF106ZQk1y3bhwDg8bH/A4Oz2tnRUJVVWs9BZa6gXZ1d3UKe89C+Mm+olGBELvA==", + "license": "ISC", + "dependencies": { + "@mapbox/jsonlint-lines-primitives": "^2.0.3", + "@mapbox/unitbezier": "^1.0.0", + "json-stringify-pretty-compact": "^4.0.0", + "minimist": "^1.2.8", + "quickselect": "^3.0.0", + "tinyqueue": "^3.0.0" + }, + "bin": { + "gl-style-format": "dist/gl-style-format.mjs", + "gl-style-migrate": "dist/gl-style-migrate.mjs", + "gl-style-validate": "dist/gl-style-validate.mjs" + } + }, + "node_modules/@maplibre/mlt": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@maplibre/mlt/-/mlt-1.3.0.tgz", + "integrity": "sha512-7M2O7ABM80Oi8XPkfkrjKB8mOBIxGoVgkA52Mu4RMWoBvvMdagQ522+WxVYujZsMpqIr3BpzrTbXrm4k8lCzCQ==", + "license": "(MIT OR Apache-2.0)", + "dependencies": { + "@mapbox/point-geometry": "^1.1.0" + } + }, + "node_modules/@maplibre/vt-pbf": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@maplibre/vt-pbf/-/vt-pbf-4.3.2.tgz", + "integrity": "sha512-j6p0AdjvAR19Z3XaCysle7A4ZSo08tYOzxD0Y9NQylwPAkwJJeYub5b2eVucdeDh7erhv69DahoLOevDRERRUw==", + "license": "MIT", + "dependencies": { + "@mapbox/point-geometry": "^1.1.0", + "@types/geojson": "^7946.0.16", + "pbf": "^5.1.0" + } + }, "node_modules/@napi-rs/lzma-linux-x64-gnu": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", @@ -1973,6 +2062,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2450,6 +2545,15 @@ "node": ">=6.0.0" } }, + "node_modules/bidi-js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.1.0.tgz", + "integrity": "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==", + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -2783,6 +2887,12 @@ "csstype": "^3.0.2" } }, + "node_modules/earcut": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.2.3.tgz", + "integrity": "sha512-vnS4AVwp1KHAF13i1vp1/2D5evWy3k5u/iW/B81QVsUZtV8cv2tU0b2VNFlqvh4kYwrFMDdjPCfAmfyJW9y14Q==", + "license": "ISC" + }, "node_modules/electron-to-chromium": { "version": "1.5.407", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.407.tgz", @@ -3157,6 +3267,12 @@ "reusify": "^1.0.4" } }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -3310,6 +3426,12 @@ "node": ">=6.9.0" } }, + "node_modules/gl-matrix": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz", + "integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==", + "license": "MIT" + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -3637,6 +3759,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stringify-pretty-compact": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz", + "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==", + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -3650,6 +3778,12 @@ "node": ">=6" } }, + "node_modules/kdbush": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.1.0.tgz", + "integrity": "sha512-e9vurzrXJQrFX6ckpHP3bvj5l+9CnYzkxDNnNQ1h2QTqdWsUAJgXiKdGNcOa1EY85dU8KbQ+z/FdQdB7P+9yfQ==", + "license": "ISC" + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -3753,6 +3887,39 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/maplibre-gl": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-6.10.0.tgz", + "integrity": "sha512-pZIJ1/rsB1jDjT3eBTfMaeSY45lRKdCuZtqXqTnkUrn0UJO9IwtbywT5ym3/cz57MGEELwAwQSYjB/Fj9TlktQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@mapbox/point-geometry": "^1.1.0", + "@mapbox/tiny-sdf": "^2.2.0", + "@mapbox/unitbezier": "^1.0.0", + "@mapbox/vector-tile": "^3.0.0", + "@maplibre/geojson-vt": "^6.1.1", + "@maplibre/maplibre-gl-style-spec": "^26.4.4", + "@maplibre/mlt": "^1.3.0", + "@maplibre/vt-pbf": "^4.3.2", + "@types/geojson": "^7946.0.16", + "bidi-js": "^1.1.0", + "earcut": "^3.2.3", + "gl-matrix": "^3.4.4", + "kdbush": "^4.1.0", + "murmurhash-js": "^1.0.0", + "pbf": "^5.1.2", + "potpack": "^2.1.0", + "quickselect": "^3.0.0", + "tinyqueue": "^3.0.0" + }, + "engines": { + "node": ">=16.14.0", + "npm": ">=8.1.0" + }, + "funding": { + "url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1" + } + }, "node_modules/memoize-one": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", @@ -3796,6 +3963,15 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/motion-dom": { "version": "11.18.1", "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz", @@ -3817,6 +3993,12 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/murmurhash-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz", + "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==", + "license": "MIT" + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -4042,6 +4224,18 @@ "node": ">=8" } }, + "node_modules/pbf": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-5.1.2.tgz", + "integrity": "sha512-mnvGdvOrIvJOBGUEdGkrVXjN8E/VkIJCkf2eS1DH2yv82ORUlLttmDt0rWY38yYZmVwciZwBUvHM20qxBZf40w==", + "license": "BSD-3-Clause", + "dependencies": { + "resolve-protobuf-schema": "^2.1.0" + }, + "bin": { + "pbf": "bin/pbf" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -4081,6 +4275,15 @@ "node": ">= 6" } }, + "node_modules/pmtiles": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/pmtiles/-/pmtiles-4.5.0.tgz", + "integrity": "sha512-CBeD4SoUluFziGdy/8k7FOjQxQy486n+929W/tWophauvMICpkZ26vGBWhDt/6b1FZQWNaf4jFdT/5+q0Wii5w==", + "license": "BSD-3-Clause", + "dependencies": { + "fflate": "^0.8.2" + } + }, "node_modules/postcss": { "version": "8.5.26", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", @@ -4253,6 +4456,12 @@ "dev": true, "license": "MIT" }, + "node_modules/potpack": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.1.0.tgz", + "integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==", + "license": "ISC" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -4413,6 +4622,12 @@ "prosemirror-transform": "^1.1.0" } }, + "node_modules/protocol-buffers-schema": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.1.tgz", + "integrity": "sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -4444,6 +4659,12 @@ ], "license": "MIT" }, + "node_modules/quickselect": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", + "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", + "license": "ISC" + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -4501,6 +4722,15 @@ "react": "^16.8.0 || ^17 || ^18 || ^19" } }, + "node_modules/react-icons": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.7.0.tgz", + "integrity": "sha512-LBLy340Rzqy6+/yVhZKT3B/QpP1BZaesGqasf09HPOBzRarcDIFH0WwXlXQfE7q7ipxK4MSiC5DIBWURCny6fw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -4623,6 +4853,15 @@ "node": ">=8.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.12", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", @@ -4653,6 +4892,15 @@ "node": ">=4" } }, + "node_modules/resolve-protobuf-schema": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", + "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==", + "license": "MIT", + "dependencies": { + "protocol-buffers-schema": "^3.3.1" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -5046,6 +5294,12 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinyqueue": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", + "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", + "license": "ISC" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index d771d65..d8726e0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,6 +6,7 @@ "scripts": { "dev": "npx vite", "build": "npx vite build", + "build:map-worker": "esbuild src/map/pmtiles.worker.ts --bundle --format=esm --outfile=public/map/pmtiles-worker.mjs", "lint": "eslint . --ext .js,.jsx,.ts,.tsx", "preview": "npx vite preview" }, @@ -19,9 +20,12 @@ "@tiptap/static-renderer": "^3.31.3", "framer-motion": "^11.5.4", "lucide-react": "0.522.0", + "maplibre-gl": "^6.10.0", + "pmtiles": "^4.5.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-hook-form": "^7.88.0", + "react-icons": "^5.7.0", "react-multi-date-picker": "^4.5.2", "react-router-dom": "^6.26.2", "react-select": "^5.10.2", diff --git a/frontend/public/map/pmtiles-worker.mjs b/frontend/public/map/pmtiles-worker.mjs new file mode 100644 index 0000000..2769c54 --- /dev/null +++ b/frontend/public/map/pmtiles-worker.mjs @@ -0,0 +1,836 @@ +// node_modules/fflate/esm/browser.js +var u8 = Uint8Array; +var u16 = Uint16Array; +var i32 = Int32Array; +var fleb = new u8([ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 2, + 2, + 2, + 2, + 3, + 3, + 3, + 3, + 4, + 4, + 4, + 4, + 5, + 5, + 5, + 5, + 0, + /* unused */ + 0, + 0, + /* impossible */ + 0 +]); +var fdeb = new u8([ + 0, + 0, + 0, + 0, + 1, + 1, + 2, + 2, + 3, + 3, + 4, + 4, + 5, + 5, + 6, + 6, + 7, + 7, + 8, + 8, + 9, + 9, + 10, + 10, + 11, + 11, + 12, + 12, + 13, + 13, + /* unused */ + 0, + 0 +]); +var clim = new u8([16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]); +var freb = function(eb, start) { + var b2 = new u16(31); + for (var i = 0; i < 31; ++i) { + b2[i] = start += 1 << eb[i - 1]; + } + var r = new i32(b2[30]); + for (var i = 1; i < 30; ++i) { + for (var j2 = b2[i]; j2 < b2[i + 1]; ++j2) { + r[j2] = j2 - b2[i] << 5 | i; + } + } + return { b: b2, r }; +}; +var _a = freb(fleb, 2); +var fl = _a.b; +var revfl = _a.r; +fl[28] = 258, revfl[258] = 28; +var _b = freb(fdeb, 0); +var fd = _b.b; +var revfd = _b.r; +var rev = new u16(32768); +for (i = 0; i < 32768; ++i) { + x2 = (i & 43690) >> 1 | (i & 21845) << 1; + x2 = (x2 & 52428) >> 2 | (x2 & 13107) << 2; + x2 = (x2 & 61680) >> 4 | (x2 & 3855) << 4; + rev[i] = ((x2 & 65280) >> 8 | (x2 & 255) << 8) >> 1; +} +var x2; +var i; +var hMap = function(cd, mb, r) { + var s = cd.length; + var i = 0; + var l = new u16(mb); + for (; i < s; ++i) { + if (cd[i]) + ++l[cd[i] - 1]; + } + var le = new u16(mb); + for (i = 1; i < mb; ++i) { + le[i] = le[i - 1] + l[i - 1] << 1; + } + var co; + if (r) { + co = new u16(1 << mb); + var rvb = 15 - mb; + for (i = 0; i < s; ++i) { + if (cd[i]) { + var sv = i << 4 | cd[i]; + var r_1 = mb - cd[i]; + var v2 = le[cd[i] - 1]++ << r_1; + for (var m = v2 | (1 << r_1) - 1; v2 <= m; ++v2) { + co[rev[v2] >> rvb] = sv; + } + } + } + } else { + co = new u16(s); + for (i = 0; i < s; ++i) { + if (cd[i]) { + co[i] = rev[le[cd[i] - 1]++] >> 15 - cd[i]; + } + } + } + return co; +}; +var flt = new u8(288); +for (i = 0; i < 144; ++i) + flt[i] = 8; +var i; +for (i = 144; i < 256; ++i) + flt[i] = 9; +var i; +for (i = 256; i < 280; ++i) + flt[i] = 7; +var i; +for (i = 280; i < 288; ++i) + flt[i] = 8; +var i; +var fdt = new u8(32); +for (i = 0; i < 32; ++i) + fdt[i] = 5; +var i; +var flrm = /* @__PURE__ */ hMap(flt, 9, 1); +var fdrm = /* @__PURE__ */ hMap(fdt, 5, 1); +var max = function(a) { + var m = a[0]; + for (var i = 1; i < a.length; ++i) { + if (a[i] > m) + m = a[i]; + } + return m; +}; +var bits = function(d2, p, m) { + var o = p / 8 | 0; + return (d2[o] | d2[o + 1] << 8) >> (p & 7) & m; +}; +var bits16 = function(d2, p) { + var o = p / 8 | 0; + return (d2[o] | d2[o + 1] << 8 | d2[o + 2] << 16) >> (p & 7); +}; +var shft = function(p) { + return (p + 7) / 8 | 0; +}; +var slc = function(v2, s, e) { + if (s == null || s < 0) + s = 0; + if (e == null || e > v2.length) + e = v2.length; + return new u8(v2.subarray(s, e)); +}; +var ec = [ + "unexpected EOF", + "invalid block type", + "invalid length/literal", + "invalid distance", + "stream finished", + "no stream handler", + , + // determined by compression function + "no callback", + "invalid UTF-8 data", + "extra field too long", + "date not in range 1980-2099", + "filename too long", + "stream finishing", + "invalid zip data" + // determined by unknown compression method +]; +var err = function(ind, msg, nt) { + var e = new Error(msg || ec[ind]); + e.code = ind; + if (Error.captureStackTrace) + Error.captureStackTrace(e, err); + if (!nt) + throw e; + return e; +}; +var inflt = function(dat, st, buf, dict) { + var sl = dat.length, dl = dict ? dict.length : 0; + if (!sl || st.f && !st.l) + return buf || new u8(0); + var noBuf = !buf; + var resize = noBuf || st.i != 2; + var noSt = st.i; + if (noBuf) + buf = new u8(sl * 3); + var cbuf = function(l2) { + var bl = buf.length; + if (l2 > bl) { + var nbuf = new u8(Math.max(bl * 2, l2)); + nbuf.set(buf); + buf = nbuf; + } + }; + var final = st.f || 0, pos = st.p || 0, bt = st.b || 0, lm = st.l, dm = st.d, lbt = st.m, dbt = st.n; + var tbts = sl * 8; + do { + if (!lm) { + final = bits(dat, pos, 1); + var type = bits(dat, pos + 1, 3); + pos += 3; + if (!type) { + var s = shft(pos) + 4, l = dat[s - 4] | dat[s - 3] << 8, t = s + l; + if (t > sl) { + if (noSt) + err(0); + break; + } + if (resize) + cbuf(bt + l); + buf.set(dat.subarray(s, t), bt); + st.b = bt += l, st.p = pos = t * 8, st.f = final; + continue; + } else if (type == 1) + lm = flrm, dm = fdrm, lbt = 9, dbt = 5; + else if (type == 2) { + var hLit = bits(dat, pos, 31) + 257, hcLen = bits(dat, pos + 10, 15) + 4; + var tl = hLit + bits(dat, pos + 5, 31) + 1; + pos += 14; + var ldt = new u8(tl); + var clt = new u8(19); + for (var i = 0; i < hcLen; ++i) { + clt[clim[i]] = bits(dat, pos + i * 3, 7); + } + pos += hcLen * 3; + var clb = max(clt), clbmsk = (1 << clb) - 1; + var clm = hMap(clt, clb, 1); + for (var i = 0; i < tl; ) { + var r = clm[bits(dat, pos, clbmsk)]; + pos += r & 15; + var s = r >> 4; + if (s < 16) { + ldt[i++] = s; + } else { + var c = 0, n = 0; + if (s == 16) + n = 3 + bits(dat, pos, 3), pos += 2, c = ldt[i - 1]; + else if (s == 17) + n = 3 + bits(dat, pos, 7), pos += 3; + else if (s == 18) + n = 11 + bits(dat, pos, 127), pos += 7; + while (n--) + ldt[i++] = c; + } + } + var lt = ldt.subarray(0, hLit), dt = ldt.subarray(hLit); + lbt = max(lt); + dbt = max(dt); + lm = hMap(lt, lbt, 1); + dm = hMap(dt, dbt, 1); + } else + err(1); + if (pos > tbts) { + if (noSt) + err(0); + break; + } + } + if (resize) + cbuf(bt + 131072); + var lms = (1 << lbt) - 1, dms = (1 << dbt) - 1; + var lpos = pos; + for (; ; lpos = pos) { + var c = lm[bits16(dat, pos) & lms], sym = c >> 4; + pos += c & 15; + if (pos > tbts) { + if (noSt) + err(0); + break; + } + if (!c) + err(2); + if (sym < 256) + buf[bt++] = sym; + else if (sym == 256) { + lpos = pos, lm = null; + break; + } else { + var add = sym - 254; + if (sym > 264) { + var i = sym - 257, b2 = fleb[i]; + add = bits(dat, pos, (1 << b2) - 1) + fl[i]; + pos += b2; + } + var d2 = dm[bits16(dat, pos) & dms], dsym = d2 >> 4; + if (!d2) + err(3); + pos += d2 & 15; + var dt = fd[dsym]; + if (dsym > 3) { + var b2 = fdeb[dsym]; + dt += bits16(dat, pos) & (1 << b2) - 1, pos += b2; + } + if (pos > tbts) { + if (noSt) + err(0); + break; + } + if (resize) + cbuf(bt + 131072); + var end = bt + add; + if (bt < dt) { + var shift = dl - dt, dend = Math.min(dt, end); + if (shift + bt < 0) + err(3); + for (; bt < dend; ++bt) + buf[bt] = dict[shift + bt]; + } + for (; bt < end; ++bt) + buf[bt] = buf[bt - dt]; + } + } + st.l = lm, st.p = lpos, st.b = bt, st.f = final; + if (lm) + final = 1, st.m = lbt, st.d = dm, st.n = dbt; + } while (!final); + return bt != buf.length && noBuf ? slc(buf, 0, bt) : buf.subarray(0, bt); +}; +var et = /* @__PURE__ */ new u8(0); +var gzs = function(d2) { + if (d2[0] != 31 || d2[1] != 139 || d2[2] != 8) + err(6, "invalid gzip data"); + var flg = d2[3]; + var st = 10; + if (flg & 4) + st += (d2[10] | d2[11] << 8) + 2; + for (var zs = (flg >> 3 & 1) + (flg >> 4 & 1); zs > 0; zs -= !d2[st++]) + ; + return st + (flg & 2); +}; +var gzl = function(d2) { + var l = d2.length; + return (d2[l - 4] | d2[l - 3] << 8 | d2[l - 2] << 16 | d2[l - 1] << 24) >>> 0; +}; +var zls = function(d2, dict) { + if ((d2[0] & 15) != 8 || d2[0] >> 4 > 7 || (d2[0] << 8 | d2[1]) % 31) + err(6, "invalid zlib data"); + if ((d2[1] >> 5 & 1) == +!dict) + err(6, "invalid zlib data: " + (d2[1] & 32 ? "need" : "unexpected") + " dictionary"); + return (d2[1] >> 3 & 4) + 2; +}; +function inflateSync(data, opts) { + return inflt(data, { i: 2 }, opts && opts.out, opts && opts.dictionary); +} +function gunzipSync(data, opts) { + var st = gzs(data); + if (st + 8 > data.length) + err(6, "invalid gzip data"); + return inflt(data.subarray(st, -8), { i: 2 }, opts && opts.out || new u8(gzl(data)), opts && opts.dictionary); +} +function unzlibSync(data, opts) { + return inflt(data.subarray(zls(data, opts && opts.dictionary), -4), { i: 2 }, opts && opts.out, opts && opts.dictionary); +} +function decompressSync(data, opts) { + return data[0] == 31 && data[1] == 139 && data[2] == 8 ? gunzipSync(data, opts) : (data[0] & 15) != 8 || data[0] >> 4 > 7 || (data[0] << 8 | data[1]) % 31 ? inflateSync(data, opts) : unzlibSync(data, opts); +} +var td = typeof TextDecoder != "undefined" && /* @__PURE__ */ new TextDecoder(); +var tds = 0; +try { + td.decode(et, { stream: true }); + tds = 1; +} catch (e) { +} + +// node_modules/pmtiles/dist/esm/index.js +var j = Object.defineProperty; +var S = Math.pow; +var d = (o, t) => j(o, "name", { value: t, configurable: true }); +var h = (o, t, e) => new Promise((r, n) => { + var s = (c) => { + try { + a(e.next(c)); + } catch (l) { + n(l); + } + }, i = (c) => { + try { + a(e.throw(c)); + } catch (l) { + n(l); + } + }, a = (c) => c.done ? r(c.value) : Promise.resolve(c.value).then(s, i); + a((e = e.apply(o, t)).next()); +}); +var re = d((o, t) => { + let e = false, r = "", n = L.GridLayer.extend({ createTile: d((s, i) => { + let a = document.createElement("img"), c = new AbortController(), l = c.signal; + return a.cancel = () => { + c.abort(); + }, e || (o.getHeader().then((u) => { + u.tileType === 1 || u.tileType === 6 ? console.error("Error: archive contains vector tiles, but leafletRasterLayer is for displaying raster tiles. See https://github.com/protomaps/PMTiles/tree/main/js for details.") : u.tileType === 2 ? r = "image/png" : u.tileType === 3 ? r = "image/jpeg" : u.tileType === 4 ? r = "image/webp" : u.tileType === 5 && (r = "image/avif"); + }), e = true), o.getZxy(s.z, s.x, s.y, l).then((u) => { + if (u) { + let m = new Blob([u.data], { type: r }), g = window.URL.createObjectURL(m); + a.src = g; + } else a.style.display = "none"; + a.cancel = void 0, i(void 0, a); + }).catch((u) => { + if (u.name !== "AbortError") throw u; + }), a; + }, "createTile"), _removeTile: d(function(s) { + let i = this._tiles[s]; + i && (i.el.cancel && i.el.cancel(), i.el.src && window.URL.revokeObjectURL(i.el.src), i.el.width = 0, i.el.height = 0, i.el.deleted = true, L.DomUtil.remove(i.el), delete this._tiles[s], this.fire("tileunload", { tile: i.el, coords: this._keyToTileCoords(s) })); + }, "_removeTile") }); + return new n(t); +}, "leafletRasterLayer"); +var z = d((o) => (t, e) => { + if (e instanceof AbortController) return o(t, e); + let r = new AbortController(); + return o(t, r).then((n) => e(void 0, n.data, n.cacheControl || "", n.expires || ""), (n) => e(n)).catch((n) => e(n)), { cancel: d(() => r.abort(), "cancel") }; +}, "v3compat"); +var A = class A2 { + constructor(t) { + this.tilev4 = d((t2, e) => h(this, null, function* () { + if (t2.type === "json") { + let g = t2.url.substr(10), p = this.tiles.get(g); + if (p || (p = new w(g), this.tiles.set(g, p)), this.metadata) { + let K = yield p.getTileJson(t2.url); + return e.signal.throwIfAborted(), { data: K }; + } + let f = yield p.getHeader(); + return e.signal.throwIfAborted(), (f.minLon >= f.maxLon || f.minLat >= f.maxLat) && console.error(`Bounds of PMTiles archive ${f.minLon},${f.minLat},${f.maxLon},${f.maxLat} are not valid.`), { data: { tiles: [`${t2.url}/{z}/{x}/{y}`], minzoom: f.minZoom, maxzoom: f.maxZoom, bounds: [f.minLon, f.minLat, f.maxLon, f.maxLat] } }; + } + let r = new RegExp(/pmtiles:\/\/(.+)\/(\d+)\/(\d+)\/(\d+)/), n = t2.url.match(r); + if (!n) throw new Error("Invalid PMTiles protocol URL"); + let s = n[1], i = this.tiles.get(s); + i || (i = new w(s), this.tiles.set(s, i)); + let a = n[2], c = n[3], l = n[4], u = yield i == null ? void 0 : i.getZxy(+a, +c, +l, e.signal); + if (e.signal.throwIfAborted(), u) return { data: new Uint8Array(u.data), cacheControl: u.cacheControl, expires: u.expires }; + let m = yield i.getHeader(); + if (m.tileType === 1 || m.tileType === 6) { + if (this.errorOnMissingTile) throw new Error("Tile not found."); + return { data: new Uint8Array() }; + } + return { data: null }; + }), "tilev4"); + this.tile = z(this.tilev4); + this.tiles = /* @__PURE__ */ new Map(), this.metadata = (t == null ? void 0 : t.metadata) || false, this.errorOnMissingTile = (t == null ? void 0 : t.errorOnMissingTile) || false; + } + add(t) { + this.tiles.set(t.source.getKey(), t); + } + get(t) { + return this.tiles.get(t); + } +}; +d(A, "Protocol"); +var B = A; +function b(o, t) { + return (t >>> 0) * 4294967296 + (o >>> 0); +} +d(b, "toNum"); +function N(o, t) { + let e = t.buf, r = e[t.pos++], n = (r & 112) >> 4; + if (r < 128 || (r = e[t.pos++], n |= (r & 127) << 3, r < 128) || (r = e[t.pos++], n |= (r & 127) << 10, r < 128) || (r = e[t.pos++], n |= (r & 127) << 17, r < 128) || (r = e[t.pos++], n |= (r & 127) << 24, r < 128) || (r = e[t.pos++], n |= (r & 1) << 31, r < 128)) return b(o, n); + throw new Error("Expected varint not more than 10 bytes"); +} +d(N, "readVarintRemainder"); +function x(o) { + let t = o.buf, e = t[o.pos++], r = e & 127; + return e < 128 || (e = t[o.pos++], r |= (e & 127) << 7, e < 128) || (e = t[o.pos++], r |= (e & 127) << 14, e < 128) || (e = t[o.pos++], r |= (e & 127) << 21, e < 128) ? r : (e = t[o.pos], r |= (e & 15) << 28, N(r, o)); +} +d(x, "readVarint"); +function I(o, t, e, r, n) { + return n === 0 ? r !== 0 ? [o - 1 - e, o - 1 - t] : [e, t] : [t, e]; +} +d(I, "rotate"); +function q(o, t, e) { + if (o > 26) throw new Error("Tile zoom level exceeds max safe number limit (26)"); + if (t >= 1 << o || e >= 1 << o) throw new Error("tile x/y outside zoom level bounds"); + let r = ((1 << o) * (1 << o) - 1) / 3, n = o - 1, [s, i] = [t, e]; + for (let a = 1 << n; a > 0; a >>= 1) { + let c = s & a, l = i & a; + r += (3 * c ^ l) * (1 << n), [s, i] = I(a, s, i, c, l), n--; + } + return r; +} +d(q, "zxyToTileId"); +function G(o) { + let t = 3 * o + 1; + return t < 4294967296 ? 31 - Math.clz32(t) : 63 - Math.clz32(t / 4294967296); +} +d(G, "tileIdToZ"); +function ie(o) { + let t = G(o) >> 1; + if (t > 26) throw new Error("Tile zoom level exceeds max safe number limit (26)"); + let e = ((1 << t) * (1 << t) - 1) / 3, r = o - e, n = 0, s = 0, i = 1 << t; + for (let a = 1; a < i; a <<= 1) { + let c = a & r / 2, l = a & (r ^ c); + [n, s] = I(a, n, s, c, l), r = r / 2, n += c, s += l; + } + return [t, n, s]; +} +d(ie, "tileIdToZxy"); +var J = ((s) => (s[s.Unknown = 0] = "Unknown", s[s.None = 1] = "None", s[s.Gzip = 2] = "Gzip", s[s.Brotli = 3] = "Brotli", s[s.Zstd = 4] = "Zstd", s))(J || {}); +function P(o, t) { + return h(this, null, function* () { + if (t === 1 || t === 0) return o; + if (t === 2) { + if (typeof globalThis.DecompressionStream == "undefined") return decompressSync(new Uint8Array(o)); + let e = new Response(o).body; + if (!e) throw new Error("Failed to read response stream"); + let r = e.pipeThrough(new globalThis.DecompressionStream("gzip")); + return new Response(r).arrayBuffer(); + } + throw new Error("Compression method not supported"); + }); +} +d(P, "defaultDecompress"); +var O = ((a) => (a[a.Unknown = 0] = "Unknown", a[a.Mvt = 1] = "Mvt", a[a.Png = 2] = "Png", a[a.Jpeg = 3] = "Jpeg", a[a.Webp = 4] = "Webp", a[a.Avif = 5] = "Avif", a[a.Mlt = 6] = "Mlt", a))(O || {}); +function _(o) { + return o === 1 ? ".mvt" : o === 2 ? ".png" : o === 3 ? ".jpg" : o === 4 ? ".webp" : o === 5 ? ".avif" : o === 6 ? ".mlt" : ""; +} +d(_, "tileTypeExt"); +var Y = 127; +function Q(o, t) { + let e = 0, r = o.length - 1; + for (; e <= r; ) { + let n = r + e >> 1, s = t - o[n].tileId; + if (s > 0) e = n + 1; + else if (s < 0) r = n - 1; + else return o[n]; + } + return r >= 0 && (o[r].runLength === 0 || t - o[r].tileId < o[r].runLength) ? o[r] : null; +} +d(Q, "findTile"); +var T = class T2 { + constructor(t) { + this.file = t; + } + getKey() { + return this.file.name; + } + getBytes(t, e) { + return h(this, null, function* () { + return { data: yield this.file.slice(t, t + e).arrayBuffer() }; + }); + } +}; +d(T, "FileSource"); +var D = class D2 { + constructor(t, e = new Headers(), r = void 0) { + var a, c; + this.url = t, this.customHeaders = e, this.credentials = r, this.mustReload = false; + let n = ""; + "navigator" in globalThis && (n = (c = (a = globalThis.navigator) == null ? void 0 : a.userAgent) != null ? c : ""); + let s = n.indexOf("Windows") > -1, i = /Chrome|Chromium|Edg|OPR|Brave/.test(n); + this.chromeWindowsNoCache = false, s && i && (this.chromeWindowsNoCache = true); + } + getKey() { + return this.url; + } + setHeaders(t) { + this.customHeaders = t; + } + getBytes(t, e, r, n) { + return h(this, null, function* () { + let s, i; + r ? i = r : (s = new AbortController(), i = s.signal); + let a = new Headers(this.customHeaders); + a.set("range", `bytes=${t}-${t + e - 1}`); + let c; + this.mustReload ? c = "reload" : this.chromeWindowsNoCache && (c = "no-store"); + let l = yield fetch(this.url, { signal: i, cache: c, headers: a, credentials: this.credentials }); + if (t === 0 && l.status === 416) { + let p = l.headers.get("Content-Range"); + if (!p || !p.startsWith("bytes */")) throw new Error("Missing content-length on 416 response"); + let f = +p.substr(8); + a.set("range", `bytes=0-${f - 1}`), l = yield fetch(this.url, { signal: i, cache: "reload", headers: a, credentials: this.credentials }); + } + let u = l.headers.get("Etag"); + if (u != null && u.startsWith("W/") && (u = null), l.status === 416 || n && u && u !== n) throw this.mustReload = true, new v(`Server returned non-matching ETag ${n} after one retry. Check browser extensions and servers for issues that may affect correct ETag headers.`); + if (l.status >= 300) throw new Error(`Bad response code: ${l.status}`); + let m = l.headers.get("Content-Length"); + if (l.status === 200 && (!m || +m > e)) throw s && s.abort(), new Error("Server returned no content-length header or content-length exceeding request. Check that your storage backend supports HTTP Byte Serving."); + return { data: yield l.arrayBuffer(), etag: u || void 0, cacheControl: l.headers.get("Cache-Control") || void 0, expires: l.headers.get("Expires") || void 0 }; + }); + } +}; +d(D, "FetchSource"); +var E = D; +function y(o, t) { + let e = o.getUint32(t + 4, true), r = o.getUint32(t + 0, true); + return e * S(2, 32) + r; +} +d(y, "getUint64"); +function X(o, t) { + let e = new DataView(o), r = e.getUint8(7); + if (r > 3) throw new Error(`Archive is spec version ${r} but this library supports up to spec version 3`); + return { specVersion: r, rootDirectoryOffset: y(e, 8), rootDirectoryLength: y(e, 16), jsonMetadataOffset: y(e, 24), jsonMetadataLength: y(e, 32), leafDirectoryOffset: y(e, 40), leafDirectoryLength: y(e, 48), tileDataOffset: y(e, 56), tileDataLength: y(e, 64), numAddressedTiles: y(e, 72), numTileEntries: y(e, 80), numTileContents: y(e, 88), clustered: e.getUint8(96) === 1, internalCompression: e.getUint8(97), tileCompression: e.getUint8(98), tileType: e.getUint8(99), minZoom: e.getUint8(100), maxZoom: e.getUint8(101), minLon: e.getInt32(102, true) / 1e7, minLat: e.getInt32(106, true) / 1e7, maxLon: e.getInt32(110, true) / 1e7, maxLat: e.getInt32(114, true) / 1e7, centerZoom: e.getUint8(118), centerLon: e.getInt32(119, true) / 1e7, centerLat: e.getInt32(123, true) / 1e7, etag: t }; +} +d(X, "bytesToHeader"); +function Z(o) { + let t = { buf: new Uint8Array(o), pos: 0 }, e = x(t), r = [], n = 0; + for (let s = 0; s < e; s++) { + let i = x(t); + r.push({ tileId: n + i, offset: 0, length: 0, runLength: 1 }), n += i; + } + for (let s = 0; s < e; s++) r[s].runLength = x(t); + for (let s = 0; s < e; s++) r[s].length = x(t); + for (let s = 0; s < e; s++) { + let i = x(t); + i === 0 && s > 0 ? r[s].offset = r[s - 1].offset + r[s - 1].length : r[s].offset = i - 1; + } + return r; +} +d(Z, "deserializeIndex"); +var R = class R2 extends Error { +}; +d(R, "EtagMismatch"); +var v = R; +function V(o, t) { + return h(this, null, function* () { + let e = yield o.getBytes(0, 16384); + if (new DataView(e.data).getUint16(0, true) !== 19792) throw new Error("Wrong magic number for PMTiles archive"); + let n = e.data.slice(0, Y), s = X(n, e.etag), i = e.data.slice(s.rootDirectoryOffset, s.rootDirectoryOffset + s.rootDirectoryLength), a = `${o.getKey()}|${s.etag || ""}|${s.rootDirectoryOffset}|${s.rootDirectoryLength}`, c = Z(yield t(i, s.internalCompression)); + return [s, [a, c.length, c]]; + }); +} +d(V, "getHeaderAndRoot"); +function F(o, t, e, r, n, s) { + return h(this, null, function* () { + let i = yield o.getBytes(e, r, s, n.etag), a = yield t(i.data, n.internalCompression), c = Z(a); + if (c.length === 0) throw new Error("Empty directory is invalid"); + return c; + }); +} +d(F, "getDirectory"); +var U = class U2 { + constructor(t = 100, e = true, r = P) { + this.cache = /* @__PURE__ */ new Map(), this.maxCacheEntries = t, this.counter = 1, this.decompress = r; + } + getHeader(t) { + return h(this, null, function* () { + let e = t.getKey(), r = this.cache.get(e); + if (r) return r.lastUsed = this.counter++, r.data; + let n = yield V(t, this.decompress); + return n[1] && this.cache.set(n[1][0], { lastUsed: this.counter++, data: n[1][2] }), this.cache.set(e, { lastUsed: this.counter++, data: n[0] }), this.prune(), n[0]; + }); + } + getDirectory(t, e, r, n, s) { + return h(this, null, function* () { + let i = `${t.getKey()}|${n.etag || ""}|${e}|${r}`, a = this.cache.get(i); + if (a) return a.lastUsed = this.counter++, a.data; + let c = yield F(t, this.decompress, e, r, n, s); + return this.cache.set(i, { lastUsed: this.counter++, data: c }), this.prune(), c; + }); + } + prune() { + if (this.cache.size > this.maxCacheEntries) { + let t = 1 / 0, e; + this.cache.forEach((r, n) => { + r.lastUsed < t && (t = r.lastUsed, e = n); + }), e && this.cache.delete(e); + } + } + invalidate(t) { + return h(this, null, function* () { + this.cache.delete(t.getKey()); + }); + } +}; +d(U, "ResolvedValueCache"); +var M = class M2 { + constructor(t = 100, e = true, r = P) { + this.cache = /* @__PURE__ */ new Map(), this.invalidations = /* @__PURE__ */ new Map(), this.pendingFetches = /* @__PURE__ */ new Map(), this.maxCacheEntries = t, this.counter = 1, this.decompress = r; + } + getHeader(t) { + return h(this, null, function* () { + let e = t.getKey(), r = this.cache.get(e); + if (r) return r.lastUsed = this.counter++, yield r.data; + let n = new Promise((s, i) => { + V(t, this.decompress).then((a) => { + a[1] && this.cache.set(a[1][0], { lastUsed: this.counter++, data: Promise.resolve(a[1][2]) }), s(a[0]), this.prune(); + }).catch((a) => { + i(a); + }); + }); + return this.cache.set(e, { lastUsed: this.counter++, data: n }), n; + }); + } + trackSignal(t, e, r) { + e.refs++, r.addEventListener("abort", () => { + --e.refs <= 0 && this.pendingFetches.get(t) === e && (e.controller.abort(), this.cache.delete(t), this.pendingFetches.delete(t)); + }, { once: true }); + } + getDirectory(t, e, r, n, s) { + return h(this, null, function* () { + let i = `${t.getKey()}|${n.etag || ""}|${e}|${r}`, a = this.cache.get(i); + if (a) { + a.lastUsed = this.counter++; + let m = this.pendingFetches.get(i); + return m && this.trackSignal(i, m, s != null ? s : new AbortController().signal), yield a.data; + } + let c = new AbortController(), l = { controller: c, refs: 0 }; + this.trackSignal(i, l, s != null ? s : new AbortController().signal), this.pendingFetches.set(i, l); + let u = new Promise((m, g) => { + F(t, this.decompress, e, r, n, c.signal).then((p) => { + this.pendingFetches.delete(i), m(p), this.prune(); + }).catch((p) => { + g(p); + }); + }); + return this.cache.set(i, { lastUsed: this.counter++, data: u }), u; + }); + } + prune() { + if (this.cache.size >= this.maxCacheEntries) { + let t = 1 / 0, e; + this.cache.forEach((r, n) => { + r.lastUsed < t && (t = r.lastUsed, e = n); + }), e && this.cache.delete(e); + } + } + invalidate(t) { + return h(this, null, function* () { + let e = t.getKey(); + if (this.invalidations.get(e)) return yield this.invalidations.get(e); + this.cache.delete(t.getKey()); + let r = new Promise((n, s) => { + this.getHeader(t).then((i) => { + n(), this.invalidations.delete(e); + }).catch((i) => { + s(i); + }); + }); + this.invalidations.set(e, r); + }); + } +}; +d(M, "SharedPromiseCache"); +var C = M; +var H = class H2 { + constructor(t, e, r) { + typeof t == "string" ? this.source = new E(t) : this.source = t, r ? this.decompress = r : this.decompress = P, e ? this.cache = e : this.cache = new C(); + } + getHeader() { + return h(this, null, function* () { + return yield this.cache.getHeader(this.source); + }); + } + getZxyAttempt(t, e, r, n) { + return h(this, null, function* () { + let s = q(t, e, r), i = yield this.cache.getHeader(this.source); + if (n == null || n.throwIfAborted(), t < i.minZoom || t > i.maxZoom) return; + let a = i.rootDirectoryOffset, c = i.rootDirectoryLength; + for (let l = 0; l <= 3; l++) { + let u = yield this.cache.getDirectory(this.source, a, c, i, n); + n == null || n.throwIfAborted(); + let m = Q(u, s); + if (m) { + if (m.runLength > 0) { + let g = yield this.source.getBytes(i.tileDataOffset + m.offset, m.length, n, i.etag); + return { data: yield this.decompress(g.data, i.tileCompression), cacheControl: g.cacheControl, expires: g.expires }; + } + a = i.leafDirectoryOffset + m.offset, c = m.length; + } else return; + } + throw new Error("Maximum directory depth exceeded"); + }); + } + getZxy(t, e, r, n) { + return h(this, null, function* () { + try { + return yield this.getZxyAttempt(t, e, r, n); + } catch (s) { + if (s instanceof v) return this.cache.invalidate(this.source), yield this.getZxyAttempt(t, e, r, n); + throw s; + } + }); + } + getMetadataAttempt() { + return h(this, null, function* () { + let t = yield this.cache.getHeader(this.source), e = yield this.source.getBytes(t.jsonMetadataOffset, t.jsonMetadataLength, void 0, t.etag), r = yield this.decompress(e.data, t.internalCompression), n = new TextDecoder("utf-8"); + return JSON.parse(n.decode(r)); + }); + } + getMetadata() { + return h(this, null, function* () { + try { + return yield this.getMetadataAttempt(); + } catch (t) { + if (t instanceof v) return this.cache.invalidate(this.source), yield this.getMetadataAttempt(); + throw t; + } + }); + } + getTileJson(t) { + return h(this, null, function* () { + let e = yield this.getHeader(), r = yield this.getMetadata(), n = _(e.tileType); + return { tilejson: "3.0.0", scheme: "xyz", tiles: [`${t}/{z}/{x}/{y}${n}`], vector_layers: r.vector_layers, attribution: r.attribution, description: r.description, name: r.name, version: r.version, bounds: [e.minLon, e.minLat, e.maxLon, e.maxLat], center: [e.centerLon, e.centerLat, e.centerZoom], minzoom: e.minZoom, maxzoom: e.maxZoom }; + }); + } +}; +d(H, "PMTiles"); +var w = H; + +// src/map/pmtiles.worker.ts +var workerScope = self; +workerScope.addProtocol("pmtiles", new B().tile); diff --git a/frontend/public/seed-media/2026/09/1.png b/frontend/public/seed-media/2026/09/1.png new file mode 100644 index 0000000..63f7ba7 Binary files /dev/null and b/frontend/public/seed-media/2026/09/1.png differ diff --git a/frontend/public/seed-media/2026/09/2.png b/frontend/public/seed-media/2026/09/2.png new file mode 100644 index 0000000..bc3e769 Binary files /dev/null and b/frontend/public/seed-media/2026/09/2.png differ diff --git a/frontend/public/seed-media/2026/09/3.png b/frontend/public/seed-media/2026/09/3.png new file mode 100644 index 0000000..a68ebdf Binary files /dev/null and b/frontend/public/seed-media/2026/09/3.png differ diff --git a/frontend/public/seed-media/2026/09/4.png b/frontend/public/seed-media/2026/09/4.png new file mode 100644 index 0000000..0011d9f Binary files /dev/null and b/frontend/public/seed-media/2026/09/4.png differ diff --git a/frontend/public/seed-media/2026/09/5.png b/frontend/public/seed-media/2026/09/5.png new file mode 100644 index 0000000..1bd17d3 Binary files /dev/null and b/frontend/public/seed-media/2026/09/5.png differ diff --git a/frontend/public/seed-media/2026/09/6.png b/frontend/public/seed-media/2026/09/6.png new file mode 100644 index 0000000..e733992 Binary files /dev/null and b/frontend/public/seed-media/2026/09/6.png differ diff --git a/seed-media/site/before-after/after.webp b/frontend/public/seed-media/site/before-after/after.webp similarity index 100% rename from seed-media/site/before-after/after.webp rename to frontend/public/seed-media/site/before-after/after.webp diff --git a/seed-media/site/before-after/before.webp b/frontend/public/seed-media/site/before-after/before.webp similarity index 100% rename from seed-media/site/before-after/before.webp rename to frontend/public/seed-media/site/before-after/before.webp diff --git a/frontend/scripts/map-palette/style.backup.json b/frontend/scripts/map-palette/style.backup.json new file mode 100644 index 0000000..9483fd4 --- /dev/null +++ b/frontend/scripts/map-palette/style.backup.json @@ -0,0 +1,2185 @@ +{ + "version": 8, + "name": "Dark Matter", + "metadata": { + "mapbox:autocomposite": false, + "mapbox:groups": { + "101da9f13b64a08fa4b6ac1168e89e5f": { + "collapsed": false, + "name": "Places" + }, + "a14c9607bc7954ba1df7205bf660433f": { + "name": "Boundaries" + }, + "b6371a3f2f5a9932464fa3867530a2e5": { + "collapsed": false, + "name": "Transportation" + } + }, + "mapbox:type": "template", + "openmaptiles:mapbox:owner": "openmaptiles", + "openmaptiles:mapbox:source:url": "mapbox://openmaptiles.4qljc88t", + "openmaptiles:version": "3.x" + }, + "sources": { + "openmaptiles": { + "type": "vector", + "url": "mbtiles://{v3}" + } + }, + "sprite": "{styleJsonFolder}/sprite", + "glyphs": "v2.0/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "rgb(12,12,12)" + } + }, + { + "id": "water", + "type": "fill", + "source": "openmaptiles", + "source-layer": "water", + "filter": [ + "all", + [ + "==", + "$type", + "Polygon" + ], + [ + "!=", + "brunnel", + "tunnel" + ] + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-antialias": false, + "fill-color": "rgb(27 ,27 ,29)" + } + }, + { + "id": "landcover_ice_shelf", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landcover", + "maxzoom": 8, + "filter": [ + "all", + [ + "==", + "$type", + "Polygon" + ], + [ + "==", + "subclass", + "ice_shelf" + ] + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-color": "rgb(12,12,12)", + "fill-opacity": 0.7 + } + }, + { + "id": "landcover_glacier", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landcover", + "maxzoom": 8, + "filter": [ + "all", + [ + "==", + "$type", + "Polygon" + ], + [ + "==", + "subclass", + "glacier" + ] + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-color": "hsl(0, 1%, 2%)", + "fill-opacity": { + "base": 1, + "stops": [ + [ + 0, + 1 + ], + [ + 8, + 0.5 + ] + ] + } + } + }, + { + "id": "landuse_residential", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landuse", + "maxzoom": 9, + "filter": [ + "all", + [ + "==", + "$type", + "Polygon" + ], + [ + "==", + "class", + "residential" + ] + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-color": "hsl(0, 2%, 5%)", + "fill-opacity": 0.4 + } + }, + { + "id": "landcover_wood", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landcover", + "minzoom": 10, + "filter": [ + "all", + [ + "==", + "$type", + "Polygon" + ], + [ + "==", + "class", + "wood" + ] + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-color": "rgb(32,32,32)", + "fill-opacity": { + "base": 0.3, + "stops": [ + [ + 8, + 0 + ], + [ + 10, + 0.8 + ], + [ + 13, + 0.4 + ] + ] + }, + "fill-pattern": "wood-pattern", + "fill-translate": [ + 0, + 0 + ] + } + }, + { + "id": "landuse_park", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landuse", + "filter": [ + "all", + [ + "==", + "$type", + "Polygon" + ], + [ + "==", + "class", + "park" + ] + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-color": "rgb(32,32,32)" + } + }, + { + "id": "waterway", + "type": "line", + "source": "openmaptiles", + "source-layer": "waterway", + "filter": [ + "==", + "$type", + "LineString" + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "line-color": "rgb(27 ,27 ,29)" + } + }, + { + "id": "water_name", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "water_name", + "filter": [ + "==", + "$type", + "LineString" + ], + "layout": { + "symbol-placement": "line", + "symbol-spacing": 500, + "text-field": [ + "coalesce", + [ + "get", + "name:fa" + ], + [ + "get", + "name" + ] + ], + "text-font": [ + "Metropolis Medium Italic", + "Noto Sans Italic" + ], + "text-rotation-alignment": "map", + "text-size": 12 + }, + "paint": { + "text-color": "hsla(0, 0%, 0%, 0.7)", + "text-halo-color": "hsl(0, 0%, 27%)" + } + }, + { + "id": "building", + "type": "fill", + "source": "openmaptiles", + "source-layer": "building", + "minzoom": 12, + "filter": [ + "==", + "$type", + "Polygon" + ], + "paint": { + "fill-antialias": true, + "fill-color": "rgb(10,10,10)", + "fill-outline-color": "rgb(27 ,27 ,29)" + } + }, + { + "id": "aeroway-taxiway", + "type": "line", + "metadata": { + "mapbox:group": "1444849345966.4436" + }, + "source": "openmaptiles", + "source-layer": "aeroway", + "minzoom": 12, + "filter": [ + "all", + [ + "in", + "class", + "taxiway" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "#181818", + "line-opacity": 1, + "line-width": { + "base": 1.55, + "stops": [ + [ + 13, + 1.8 + ], + [ + 20, + 20 + ] + ] + } + } + }, + { + "id": "aeroway-runway-casing", + "type": "line", + "metadata": { + "mapbox:group": "1444849345966.4436" + }, + "source": "openmaptiles", + "source-layer": "aeroway", + "minzoom": 11, + "filter": [ + "all", + [ + "in", + "class", + "runway" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "rgba(60,60,60,0.8)", + "line-opacity": 1, + "line-width": { + "base": 1.5, + "stops": [ + [ + 11, + 5 + ], + [ + 17, + 55 + ] + ] + } + } + }, + { + "id": "aeroway-area", + "type": "fill", + "metadata": { + "mapbox:group": "1444849345966.4436" + }, + "source": "openmaptiles", + "source-layer": "aeroway", + "minzoom": 4, + "filter": [ + "all", + [ + "==", + "$type", + "Polygon" + ], + [ + "in", + "class", + "runway", + "taxiway" + ] + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-color": "#000", + "fill-opacity": 1 + } + }, + { + "id": "aeroway-runway", + "type": "line", + "metadata": { + "mapbox:group": "1444849345966.4436" + }, + "source": "openmaptiles", + "source-layer": "aeroway", + "minzoom": 11, + "filter": [ + "all", + [ + "in", + "class", + "runway" + ], + [ + "==", + "$type", + "LineString" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "#000", + "line-opacity": 1, + "line-width": { + "base": 1.5, + "stops": [ + [ + 11, + 4 + ], + [ + 17, + 50 + ] + ] + } + } + }, + { + "id": "road_area_pier", + "type": "fill", + "metadata": {}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "==", + "$type", + "Polygon" + ], + [ + "==", + "class", + "pier" + ] + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-antialias": true, + "fill-color": "rgb(12,12,12)" + } + }, + { + "id": "road_pier", + "type": "line", + "metadata": {}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "in", + "class", + "pier" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-color": "rgb(12,12,12)", + "line-width": { + "base": 1.2, + "stops": [ + [ + 15, + 1 + ], + [ + 17, + 4 + ] + ] + } + } + }, + { + "id": "highway_path", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "==", + "class", + "path" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "rgb(27 ,27 ,29)", + "line-dasharray": [ + 1.5, + 1.5 + ], + "line-opacity": 0.9, + "line-width": { + "base": 1.2, + "stops": [ + [ + 13, + 1 + ], + [ + 20, + 10 + ] + ] + } + } + }, + { + "id": "highway_minor", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 8, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "in", + "class", + "minor", + "service", + "track" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "#181818", + "line-opacity": 0.9, + "line-width": { + "base": 1.55, + "stops": [ + [ + 13, + 1.8 + ], + [ + 20, + 20 + ] + ] + } + } + }, + { + "id": "highway_major_casing", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 11, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "in", + "class", + "primary", + "secondary", + "tertiary", + "trunk" + ] + ], + "layout": { + "line-cap": "butt", + "line-join": "miter", + "visibility": "visible" + }, + "paint": { + "line-color": "rgba(60,60,60,0.8)", + "line-dasharray": [ + 12, + 0 + ], + "line-width": { + "base": 1.3, + "stops": [ + [ + 10, + 3 + ], + [ + 20, + 23 + ] + ] + } + } + }, + { + "id": "highway_major_inner", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 11, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "in", + "class", + "primary", + "secondary", + "tertiary", + "trunk" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "hsl(0, 0%, 7%)", + "line-width": { + "base": 1.3, + "stops": [ + [ + 10, + 2 + ], + [ + 20, + 20 + ] + ] + } + } + }, + { + "id": "highway_major_subtle", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 6, + "maxzoom": 11, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "in", + "class", + "primary", + "secondary", + "tertiary", + "trunk" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "#2a2a2a", + "line-width": { + "stops": [ + [ + 6, + 0 + ], + [ + 8, + 2 + ] + ] + } + } + }, + { + "id": "highway_motorway_casing", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 6, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "==", + "class", + "motorway" + ] + ], + "layout": { + "line-cap": "butt", + "line-join": "miter", + "visibility": "visible" + }, + "paint": { + "line-color": "rgba(60,60,60,0.8)", + "line-dasharray": [ + 2, + 0 + ], + "line-opacity": 1, + "line-width": { + "base": 1.4, + "stops": [ + [ + 5.8, + 0 + ], + [ + 6, + 3 + ], + [ + 20, + 40 + ] + ] + } + } + }, + { + "id": "highway_motorway_inner", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 6, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "==", + "class", + "motorway" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": { + "base": 1, + "stops": [ + [ + 5.8, + "hsla(0, 0%, 85%, 0.53)" + ], + [ + 6, + "#000" + ] + ] + }, + "line-width": { + "base": 1.4, + "stops": [ + [ + 4, + 2 + ], + [ + 6, + 1.3 + ], + [ + 20, + 30 + ] + ] + } + } + }, + { + "id": "road_oneway", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 15, + "filter": [ + "all", + [ + "==", + "oneway", + 1 + ] + ], + "layout": { + "symbol-placement": "line", + "icon-image": "oneway", + "symbol-spacing": 200, + "icon-padding": 2, + "icon-rotation-alignment": "map", + "icon-rotate": 0, + "icon-size": { + "stops": [ + [ + 15, + 0.5 + ], + [ + 19, + 1 + ] + ] + } + }, + "paint": { + "icon-opacity": 0.5 + } + }, + { + "id": "road_oneway_opposite", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 15, + "filter": [ + "all", + [ + "==", + "oneway", + -1 + ] + ], + "layout": { + "symbol-placement": "line", + "icon-image": "oneway", + "symbol-spacing": 200, + "icon-padding": 2, + "icon-rotation-alignment": "map", + "icon-rotate": 180, + "icon-size": { + "stops": [ + [ + 15, + 0.5 + ], + [ + 19, + 1 + ] + ] + } + }, + "paint": { + "icon-opacity": 0.5 + } + }, + { + "id": "highway_motorway_subtle", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "maxzoom": 6, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "==", + "class", + "motorway" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "#181818", + "line-width": { + "base": 1.4, + "stops": [ + [ + 4, + 2 + ], + [ + 6, + 1.3 + ] + ] + } + } + }, + { + "id": "railway_transit", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 16, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "all", + [ + "==", + "class", + "transit" + ], + [ + "!in", + "brunnel", + "tunnel" + ] + ] + ], + "layout": { + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "rgb(35,35,35)", + "line-width": 3 + } + }, + { + "id": "railway_transit_dashline", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 16, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "all", + [ + "==", + "class", + "transit" + ], + [ + "!in", + "brunnel", + "tunnel" + ] + ] + ], + "layout": { + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "rgb(12,12,12)", + "line-dasharray": [ + 3, + 3 + ], + "line-width": 2 + } + }, + { + "id": "railway_minor", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 16, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "all", + [ + "==", + "class", + "rail" + ], + [ + "has", + "service" + ] + ] + ], + "layout": { + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "rgb(35,35,35)", + "line-width": 3 + } + }, + { + "id": "railway_minor_dashline", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 16, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "all", + [ + "==", + "class", + "rail" + ], + [ + "has", + "service" + ] + ] + ], + "layout": { + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "rgb(12,12,12)", + "line-dasharray": [ + 3, + 3 + ], + "line-width": 2 + } + }, + { + "id": "railway", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 13, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "==", + "class", + "rail" + ], + [ + "!has", + "service" + ] + ], + "layout": { + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "rgb(35,35,35)", + "line-width": { + "base": 1.3, + "stops": [ + [ + 16, + 3 + ], + [ + 20, + 7 + ] + ] + } + } + }, + { + "id": "railway_dashline", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 13, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "==", + "class", + "rail" + ], + [ + "!has", + "service" + ] + ], + "layout": { + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "rgb(12,12,12)", + "line-dasharray": [ + 3, + 3 + ], + "line-width": { + "base": 1.3, + "stops": [ + [ + 16, + 2 + ], + [ + 20, + 6 + ] + ] + } + } + }, + { + "id": "highway_name_other", + "type": "symbol", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation_name", + "filter": [ + "all", + [ + "!=", + "class", + "motorway" + ], + [ + "==", + "$type", + "LineString" + ] + ], + "layout": { + "symbol-placement": "line", + "symbol-spacing": 350, + "text-field": [ + "coalesce", + [ + "get", + "name:fa" + ], + [ + "get", + "name" + ] + ], + "text-font": [ + "Metropolis Regular", + "Noto Sans Regular" + ], + "text-max-angle": 30, + "text-pitch-alignment": "viewport", + "text-rotation-alignment": "map", + "text-size": 10, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "text-color": "rgba(80, 78, 78, 1)", + "text-halo-blur": 0, + "text-halo-color": "rgba(0, 0, 0, 1)", + "text-halo-width": 1, + "text-translate": [ + 0, + 0 + ] + } + }, + { + "id": "highway_name_motorway", + "type": "symbol", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation_name", + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "==", + "class", + "motorway" + ] + ], + "layout": { + "symbol-placement": "line", + "symbol-spacing": 350, + "text-field": "{ref}", + "text-font": [ + "Metropolis Light", + "Noto Sans Regular" + ], + "text-pitch-alignment": "viewport", + "text-rotation-alignment": "viewport", + "text-size": 10, + "visibility": "visible" + }, + "paint": { + "text-color": "hsl(0, 0%, 37%)", + "text-translate": [ + 0, + 2 + ] + } + }, + { + "id": "boundary_state", + "type": "line", + "metadata": { + "mapbox:group": "a14c9607bc7954ba1df7205bf660433f" + }, + "source": "openmaptiles", + "source-layer": "boundary", + "filter": [ + "==", + "admin_level", + 4 + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-blur": 0.4, + "line-color": "hsl(0, 0%, 21%)", + "line-dasharray": [ + 2, + 2 + ], + "line-opacity": 1, + "line-width": { + "base": 1.3, + "stops": [ + [ + 3, + 1 + ], + [ + 22, + 15 + ] + ] + } + } + }, + { + "id": "boundary_country_z0-4", + "type": "line", + "metadata": { + "mapbox:group": "a14c9607bc7954ba1df7205bf660433f" + }, + "source": "openmaptiles", + "source-layer": "boundary", + "maxzoom": 5, + "filter": [ + "all", + [ + "==", + "admin_level", + 2 + ], + [ + "!has", + "claimed_by" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-blur": { + "base": 1, + "stops": [ + [ + 0, + 0.4 + ], + [ + 22, + 4 + ] + ] + }, + "line-color": "hsl(0, 0%, 23%)", + "line-opacity": 1, + "line-width": { + "base": 1.1, + "stops": [ + [ + 3, + 1 + ], + [ + 22, + 20 + ] + ] + } + } + }, + { + "id": "boundary_country_z5-", + "type": "line", + "metadata": { + "mapbox:group": "a14c9607bc7954ba1df7205bf660433f" + }, + "source": "openmaptiles", + "source-layer": "boundary", + "minzoom": 5, + "filter": [ + "==", + "admin_level", + 2 + ], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-blur": { + "base": 1, + "stops": [ + [ + 0, + 0.4 + ], + [ + 22, + 4 + ] + ] + }, + "line-color": "hsl(0, 0%, 23%)", + "line-opacity": 1, + "line-width": { + "base": 1.1, + "stops": [ + [ + 3, + 1 + ], + [ + 22, + 20 + ] + ] + } + } + }, + { + "id": "place_other", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 14, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "in", + "class", + "hamlet", + "isolated_dwelling", + "neighbourhood" + ] + ], + "layout": { + "text-anchor": "center", + "text-field": [ + "coalesce", + [ + "get", + "name:fa" + ], + [ + "get", + "name" + ] + ], + "text-font": [ + "Metropolis Regular", + "Noto Sans Regular" + ], + "text-justify": "center", + "text-offset": [ + 0.5, + 0 + ], + "text-size": 10, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "text-color": "rgb(101,101,101)", + "text-halo-blur": 1, + "text-halo-color": "rgba(0,0,0,0.7)", + "text-halo-width": 1 + } + }, + { + "id": "place_suburb", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 15, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "==", + "class", + "suburb" + ] + ], + "layout": { + "text-anchor": "center", + "text-field": [ + "coalesce", + [ + "get", + "name:fa" + ], + [ + "get", + "name" + ] + ], + "text-font": [ + "Metropolis Regular", + "Noto Sans Regular" + ], + "text-justify": "center", + "text-offset": [ + 0.5, + 0 + ], + "text-size": 10, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "text-color": "rgb(101,101,101)", + "text-halo-blur": 1, + "text-halo-color": "rgba(0,0,0,0.7)", + "text-halo-width": 1 + } + }, + { + "id": "place_village", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 14, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "==", + "class", + "village" + ] + ], + "layout": { + "icon-size": 0.4, + "text-anchor": "left", + "text-field": [ + "coalesce", + [ + "get", + "name:fa" + ], + [ + "get", + "name" + ] + ], + "text-font": [ + "Metropolis Regular", + "Noto Sans Regular" + ], + "text-justify": "left", + "text-offset": [ + 0.5, + 0.2 + ], + "text-size": 10, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "icon-opacity": 0.7, + "text-color": "rgb(101,101,101)", + "text-halo-blur": 1, + "text-halo-color": "rgba(0,0,0,0.7)", + "text-halo-width": 1 + } + }, + { + "id": "place_town", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 15, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "==", + "class", + "town" + ] + ], + "layout": { + "icon-image": { + "base": 1, + "stops": [ + [ + 0, + "circle-11" + ], + [ + 9, + "" + ] + ] + }, + "icon-size": 0.4, + "text-anchor": { + "base": 1, + "stops": [ + [ + 0, + "left" + ], + [ + 8, + "center" + ] + ] + }, + "text-field": [ + "coalesce", + [ + "get", + "name:fa" + ], + [ + "get", + "name" + ] + ], + "text-font": [ + "Metropolis Regular", + "Noto Sans Regular" + ], + "text-justify": "left", + "text-offset": [ + 0.5, + 0.2 + ], + "text-size": 10, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "icon-opacity": 0.7, + "text-color": "rgb(101,101,101)", + "text-halo-blur": 1, + "text-halo-color": "rgba(0,0,0,0.7)", + "text-halo-width": 1 + } + }, + { + "id": "place_city", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 14, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "==", + "class", + "city" + ], + [ + ">", + "rank", + 3 + ] + ], + "layout": { + "icon-image": { + "base": 1, + "stops": [ + [ + 0, + "circle-11" + ], + [ + 9, + "" + ] + ] + }, + "icon-size": 0.4, + "text-anchor": { + "base": 1, + "stops": [ + [ + 0, + "left" + ], + [ + 8, + "center" + ] + ] + }, + "text-field": [ + "coalesce", + [ + "get", + "name:fa" + ], + [ + "get", + "name" + ] + ], + "text-font": [ + "Metropolis Regular", + "Noto Sans Regular" + ], + "text-justify": "left", + "text-offset": [ + 0.5, + 0.2 + ], + "text-size": 10, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "icon-opacity": 0.7, + "text-color": "rgb(101,101,101)", + "text-halo-blur": 1, + "text-halo-color": "rgba(0,0,0,0.7)", + "text-halo-width": 1 + } + }, + { + "id": "place_city_large", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 12, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "<=", + "rank", + 3 + ], + [ + "==", + "class", + "city" + ] + ], + "layout": { + "icon-image": { + "base": 1, + "stops": [ + [ + 0, + "circle-11" + ], + [ + 9, + "" + ] + ] + }, + "icon-size": 0.4, + "text-anchor": { + "base": 1, + "stops": [ + [ + 0, + "left" + ], + [ + 8, + "center" + ] + ] + }, + "text-field": [ + "coalesce", + [ + "get", + "name:fa" + ], + [ + "get", + "name" + ] + ], + "text-font": [ + "Metropolis Regular", + "Noto Sans Regular" + ], + "text-justify": "left", + "text-offset": [ + 0.5, + 0.2 + ], + "text-size": 14, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "icon-opacity": 0.7, + "text-color": "rgb(101,101,101)", + "text-halo-blur": 1, + "text-halo-color": "rgba(0,0,0,0.7)", + "text-halo-width": 1 + } + }, + { + "id": "place_state", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 12, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "==", + "class", + "state" + ] + ], + "layout": { + "text-field": [ + "coalesce", + [ + "get", + "name:fa" + ], + [ + "get", + "name" + ] + ], + "text-font": [ + "Metropolis Regular", + "Noto Sans Regular" + ], + "text-size": 10, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "text-color": "rgb(101,101,101)", + "text-halo-blur": 1, + "text-halo-color": "rgba(0,0,0,0.7)", + "text-halo-width": 1 + } + }, + { + "id": "place_country_other", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 8, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "==", + "class", + "country" + ], + [ + "!has", + "iso_a2" + ] + ], + "layout": { + "text-field": [ + "coalesce", + [ + "get", + "name:fa" + ], + [ + "get", + "name" + ] + ], + "text-font": [ + "Metropolis Light Italic", + "Noto Sans Italic" + ], + "text-size": { + "base": 1, + "stops": [ + [ + 0, + 9 + ], + [ + 1, + 11 + ] + ] + }, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "text-color": "rgb(101,101,101)", + "text-halo-color": "rgba(0,0,0,0.7)", + "text-halo-width": 1.4 + } + }, + { + "id": "place_country_minor", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 8, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "==", + "class", + "country" + ], + [ + ">=", + "rank", + 2 + ], + [ + "has", + "iso_a2" + ] + ], + "layout": { + "text-field": [ + "coalesce", + [ + "get", + "name:fa" + ], + [ + "get", + "name" + ] + ], + "text-font": [ + "Metropolis Regular", + "Noto Sans Regular" + ], + "text-size": { + "base": 1, + "stops": [ + [ + 0, + 10 + ], + [ + 6, + 12 + ] + ] + }, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "text-color": "rgb(101,101,101)", + "text-halo-color": "rgba(0,0,0,0.7)", + "text-halo-width": 1.4 + } + }, + { + "id": "place_country_major", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 6, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "<=", + "rank", + 1 + ], + [ + "==", + "class", + "country" + ], + [ + "has", + "iso_a2" + ] + ], + "layout": { + "text-anchor": "center", + "text-field": [ + "coalesce", + [ + "get", + "name:fa" + ], + [ + "get", + "name" + ] + ], + "text-font": [ + "Metropolis Regular", + "Noto Sans Regular" + ], + "text-size": { + "base": 1.4, + "stops": [ + [ + 0, + 10 + ], + [ + 3, + 12 + ], + [ + 4, + 14 + ] + ] + }, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "text-color": "rgb(101,101,101)", + "text-halo-color": "rgba(0,0,0,0.7)", + "text-halo-width": 1.4 + } + } + ] +} \ No newline at end of file diff --git a/frontend/scripts/map-palette/style.json b/frontend/scripts/map-palette/style.json new file mode 100644 index 0000000..d37492e --- /dev/null +++ b/frontend/scripts/map-palette/style.json @@ -0,0 +1,2065 @@ +{ + "version": 8, + "name": "Dark Matter", + "metadata": { + "mapbox:autocomposite": false, + "mapbox:groups": { + "101da9f13b64a08fa4b6ac1168e89e5f": { + "collapsed": false, + "name": "Places" + }, + "a14c9607bc7954ba1df7205bf660433f": { + "name": "Boundaries" + }, + "b6371a3f2f5a9932464fa3867530a2e5": { + "collapsed": false, + "name": "Transportation" + } + }, + "mapbox:type": "template", + "openmaptiles:mapbox:owner": "openmaptiles", + "openmaptiles:mapbox:source:url": "mapbox://openmaptiles.4qljc88t", + "openmaptiles:version": "3.x" + }, + "sources": { + "openmaptiles": { + "type": "vector", + "url": "mbtiles://{v3}" + } + }, + "sprite": "{styleJsonFolder}/sprite", + "glyphs": "v2.0/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "rgb(12,12,12)" + } + }, + { + "id": "water", + "type": "fill", + "source": "openmaptiles", + "source-layer": "water", + "filter": [ + "all", + [ + "==", + "$type", + "Polygon" + ], + [ + "!=", + "brunnel", + "tunnel" + ] + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-antialias": false, + "fill-color": "rgb(27 ,27 ,29)" + } + }, + { + "id": "landcover_ice_shelf", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landcover", + "maxzoom": 8, + "filter": [ + "all", + [ + "==", + "$type", + "Polygon" + ], + [ + "==", + "subclass", + "ice_shelf" + ] + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-color": "rgb(12,12,12)", + "fill-opacity": 0.7 + } + }, + { + "id": "landcover_glacier", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landcover", + "maxzoom": 8, + "filter": [ + "all", + [ + "==", + "$type", + "Polygon" + ], + [ + "==", + "subclass", + "glacier" + ] + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-color": "hsl(0, 1%, 2%)", + "fill-opacity": { + "base": 1, + "stops": [ + [ + 0, + 1 + ], + [ + 8, + 0.5 + ] + ] + } + } + }, + { + "id": "landuse_residential", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landuse", + "maxzoom": 9, + "filter": [ + "all", + [ + "==", + "$type", + "Polygon" + ], + [ + "==", + "class", + "residential" + ] + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-color": "hsl(0, 2%, 5%)", + "fill-opacity": 0.4 + } + }, + { + "id": "landcover_wood", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landcover", + "minzoom": 10, + "filter": [ + "all", + [ + "==", + "$type", + "Polygon" + ], + [ + "==", + "class", + "wood" + ] + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-color": "rgb(32,32,32)", + "fill-opacity": { + "base": 0.3, + "stops": [ + [ + 8, + 0 + ], + [ + 10, + 0.8 + ], + [ + 13, + 0.4 + ] + ] + }, + "fill-pattern": "wood-pattern", + "fill-translate": [ + 0, + 0 + ] + } + }, + { + "id": "landuse_park", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landuse", + "filter": [ + "all", + [ + "==", + "$type", + "Polygon" + ], + [ + "==", + "class", + "park" + ] + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-color": "rgb(32,32,32)" + } + }, + { + "id": "waterway", + "type": "line", + "source": "openmaptiles", + "source-layer": "waterway", + "filter": [ + "==", + "$type", + "LineString" + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "line-color": "rgb(27 ,27 ,29)" + } + }, + { + "id": "water_name", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "water_name", + "filter": [ + "==", + "$type", + "LineString" + ], + "layout": { + "symbol-placement": "line", + "symbol-spacing": 500, + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": [ + "Noto Sans Italic", + "Metropolis Medium Italic" + ], + "text-rotation-alignment": "map", + "text-size": 12 + }, + "paint": { + "text-color": "hsla(0, 0%, 0%, 0.7)", + "text-halo-color": "hsl(0, 0%, 27%)" + } + }, + { + "id": "building", + "type": "fill", + "source": "openmaptiles", + "source-layer": "building", + "minzoom": 12, + "filter": [ + "==", + "$type", + "Polygon" + ], + "paint": { + "fill-antialias": true, + "fill-color": "rgb(10,10,10)", + "fill-outline-color": "rgb(27 ,27 ,29)" + } + }, + { + "id": "aeroway-taxiway", + "type": "line", + "metadata": { + "mapbox:group": "1444849345966.4436" + }, + "source": "openmaptiles", + "source-layer": "aeroway", + "minzoom": 12, + "filter": [ + "all", + [ + "in", + "class", + "taxiway" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "#181818", + "line-opacity": 1, + "line-width": { + "base": 1.55, + "stops": [ + [ + 13, + 1.8 + ], + [ + 20, + 20 + ] + ] + } + } + }, + { + "id": "aeroway-runway-casing", + "type": "line", + "metadata": { + "mapbox:group": "1444849345966.4436" + }, + "source": "openmaptiles", + "source-layer": "aeroway", + "minzoom": 11, + "filter": [ + "all", + [ + "in", + "class", + "runway" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "rgba(60,60,60,0.8)", + "line-opacity": 1, + "line-width": { + "base": 1.5, + "stops": [ + [ + 11, + 5 + ], + [ + 17, + 55 + ] + ] + } + } + }, + { + "id": "aeroway-area", + "type": "fill", + "metadata": { + "mapbox:group": "1444849345966.4436" + }, + "source": "openmaptiles", + "source-layer": "aeroway", + "minzoom": 4, + "filter": [ + "all", + [ + "==", + "$type", + "Polygon" + ], + [ + "in", + "class", + "runway", + "taxiway" + ] + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-color": "#000", + "fill-opacity": 1 + } + }, + { + "id": "aeroway-runway", + "type": "line", + "metadata": { + "mapbox:group": "1444849345966.4436" + }, + "source": "openmaptiles", + "source-layer": "aeroway", + "minzoom": 11, + "filter": [ + "all", + [ + "in", + "class", + "runway" + ], + [ + "==", + "$type", + "LineString" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "#000", + "line-opacity": 1, + "line-width": { + "base": 1.5, + "stops": [ + [ + 11, + 4 + ], + [ + 17, + 50 + ] + ] + } + } + }, + { + "id": "road_area_pier", + "type": "fill", + "metadata": {}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "==", + "$type", + "Polygon" + ], + [ + "==", + "class", + "pier" + ] + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-antialias": true, + "fill-color": "rgb(12,12,12)" + } + }, + { + "id": "road_pier", + "type": "line", + "metadata": {}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "in", + "class", + "pier" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-color": "rgb(12,12,12)", + "line-width": { + "base": 1.2, + "stops": [ + [ + 15, + 1 + ], + [ + 17, + 4 + ] + ] + } + } + }, + { + "id": "highway_path", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "==", + "class", + "path" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "rgb(27 ,27 ,29)", + "line-dasharray": [ + 1.5, + 1.5 + ], + "line-opacity": 0.9, + "line-width": { + "base": 1.2, + "stops": [ + [ + 13, + 1 + ], + [ + 20, + 10 + ] + ] + } + } + }, + { + "id": "highway_minor", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 8, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "in", + "class", + "minor", + "service", + "track" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "#181818", + "line-opacity": 0.9, + "line-width": { + "base": 1.55, + "stops": [ + [ + 13, + 1.8 + ], + [ + 20, + 20 + ] + ] + } + } + }, + { + "id": "highway_major_casing", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 11, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "in", + "class", + "primary", + "secondary", + "tertiary", + "trunk" + ] + ], + "layout": { + "line-cap": "butt", + "line-join": "miter", + "visibility": "visible" + }, + "paint": { + "line-color": "rgba(60,60,60,0.8)", + "line-dasharray": [ + 12, + 0 + ], + "line-width": { + "base": 1.3, + "stops": [ + [ + 10, + 3 + ], + [ + 20, + 23 + ] + ] + } + } + }, + { + "id": "highway_major_inner", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 11, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "in", + "class", + "primary", + "secondary", + "tertiary", + "trunk" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "hsl(0, 0%, 7%)", + "line-width": { + "base": 1.3, + "stops": [ + [ + 10, + 2 + ], + [ + 20, + 20 + ] + ] + } + } + }, + { + "id": "highway_major_subtle", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 6, + "maxzoom": 11, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "in", + "class", + "primary", + "secondary", + "tertiary", + "trunk" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "#2a2a2a", + "line-width": { + "stops": [ + [ + 6, + 0 + ], + [ + 8, + 2 + ] + ] + } + } + }, + { + "id": "highway_motorway_casing", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 6, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "==", + "class", + "motorway" + ] + ], + "layout": { + "line-cap": "butt", + "line-join": "miter", + "visibility": "visible" + }, + "paint": { + "line-color": "rgba(60,60,60,0.8)", + "line-dasharray": [ + 2, + 0 + ], + "line-opacity": 1, + "line-width": { + "base": 1.4, + "stops": [ + [ + 5.8, + 0 + ], + [ + 6, + 3 + ], + [ + 20, + 40 + ] + ] + } + } + }, + { + "id": "highway_motorway_inner", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 6, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "==", + "class", + "motorway" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": { + "base": 1, + "stops": [ + [ + 5.8, + "hsla(0, 0%, 85%, 0.53)" + ], + [ + 6, + "#000" + ] + ] + }, + "line-width": { + "base": 1.4, + "stops": [ + [ + 4, + 2 + ], + [ + 6, + 1.3 + ], + [ + 20, + 30 + ] + ] + } + } + }, + { + "id": "road_oneway", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 15, + "filter": [ + "all", + [ + "==", + "oneway", + 1 + ] + ], + "layout": { + "symbol-placement": "line", + "icon-image": "oneway", + "symbol-spacing": 200, + "icon-padding": 2, + "icon-rotation-alignment": "map", + "icon-rotate": 0, + "icon-size": { + "stops": [ + [ + 15, + 0.5 + ], + [ + 19, + 1 + ] + ] + } + }, + "paint": { + "icon-opacity": 0.5 + } + }, + { + "id": "road_oneway_opposite", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 15, + "filter": [ + "all", + [ + "==", + "oneway", + -1 + ] + ], + "layout": { + "symbol-placement": "line", + "icon-image": "oneway", + "symbol-spacing": 200, + "icon-padding": 2, + "icon-rotation-alignment": "map", + "icon-rotate": 180, + "icon-size": { + "stops": [ + [ + 15, + 0.5 + ], + [ + 19, + 1 + ] + ] + } + }, + "paint": { + "icon-opacity": 0.5 + } + }, + { + "id": "highway_motorway_subtle", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "maxzoom": 6, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "==", + "class", + "motorway" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "#181818", + "line-width": { + "base": 1.4, + "stops": [ + [ + 4, + 2 + ], + [ + 6, + 1.3 + ] + ] + } + } + }, + { + "id": "railway_transit", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 16, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "all", + [ + "==", + "class", + "transit" + ], + [ + "!in", + "brunnel", + "tunnel" + ] + ] + ], + "layout": { + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "rgb(35,35,35)", + "line-width": 3 + } + }, + { + "id": "railway_transit_dashline", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 16, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "all", + [ + "==", + "class", + "transit" + ], + [ + "!in", + "brunnel", + "tunnel" + ] + ] + ], + "layout": { + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "rgb(12,12,12)", + "line-dasharray": [ + 3, + 3 + ], + "line-width": 2 + } + }, + { + "id": "railway_minor", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 16, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "all", + [ + "==", + "class", + "rail" + ], + [ + "has", + "service" + ] + ] + ], + "layout": { + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "rgb(35,35,35)", + "line-width": 3 + } + }, + { + "id": "railway_minor_dashline", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 16, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "all", + [ + "==", + "class", + "rail" + ], + [ + "has", + "service" + ] + ] + ], + "layout": { + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "rgb(12,12,12)", + "line-dasharray": [ + 3, + 3 + ], + "line-width": 2 + } + }, + { + "id": "railway", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 13, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "==", + "class", + "rail" + ], + [ + "!has", + "service" + ] + ], + "layout": { + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "rgb(35,35,35)", + "line-width": { + "base": 1.3, + "stops": [ + [ + 16, + 3 + ], + [ + 20, + 7 + ] + ] + } + } + }, + { + "id": "railway_dashline", + "type": "line", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 13, + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "==", + "class", + "rail" + ], + [ + "!has", + "service" + ] + ], + "layout": { + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "rgb(12,12,12)", + "line-dasharray": [ + 3, + 3 + ], + "line-width": { + "base": 1.3, + "stops": [ + [ + 16, + 2 + ], + [ + 20, + 6 + ] + ] + } + } + }, + { + "id": "highway_name_other", + "type": "symbol", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation_name", + "filter": [ + "all", + [ + "!=", + "class", + "motorway" + ], + [ + "==", + "$type", + "LineString" + ] + ], + "layout": { + "symbol-placement": "line", + "symbol-spacing": 350, + "text-field": "{name:latin} {name:nonlatin}", + "text-font": [ + "Noto Sans Regular", + "Metropolis Regular" + ], + "text-max-angle": 30, + "text-pitch-alignment": "viewport", + "text-rotation-alignment": "map", + "text-size": 10, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "text-color": "rgba(80, 78, 78, 1)", + "text-halo-blur": 0, + "text-halo-color": "rgba(0, 0, 0, 1)", + "text-halo-width": 1, + "text-translate": [ + 0, + 0 + ] + } + }, + { + "id": "highway_name_motorway", + "type": "symbol", + "metadata": { + "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" + }, + "source": "openmaptiles", + "source-layer": "transportation_name", + "filter": [ + "all", + [ + "==", + "$type", + "LineString" + ], + [ + "==", + "class", + "motorway" + ] + ], + "layout": { + "symbol-placement": "line", + "symbol-spacing": 350, + "text-field": "{ref}", + "text-font": [ + "Noto Sans Regular", + "Metropolis Light" + ], + "text-pitch-alignment": "viewport", + "text-rotation-alignment": "viewport", + "text-size": 10, + "visibility": "visible" + }, + "paint": { + "text-color": "hsl(0, 0%, 37%)", + "text-translate": [ + 0, + 2 + ] + } + }, + { + "id": "boundary_state", + "type": "line", + "metadata": { + "mapbox:group": "a14c9607bc7954ba1df7205bf660433f" + }, + "source": "openmaptiles", + "source-layer": "boundary", + "filter": [ + "==", + "admin_level", + 4 + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-blur": 0.4, + "line-color": "hsl(0, 0%, 21%)", + "line-dasharray": [ + 2, + 2 + ], + "line-opacity": 1, + "line-width": { + "base": 1.3, + "stops": [ + [ + 3, + 1 + ], + [ + 22, + 15 + ] + ] + } + } + }, + { + "id": "boundary_country_z0-4", + "type": "line", + "metadata": { + "mapbox:group": "a14c9607bc7954ba1df7205bf660433f" + }, + "source": "openmaptiles", + "source-layer": "boundary", + "maxzoom": 5, + "filter": [ + "all", + [ + "==", + "admin_level", + 2 + ], + [ + "!has", + "claimed_by" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-blur": { + "base": 1, + "stops": [ + [ + 0, + 0.4 + ], + [ + 22, + 4 + ] + ] + }, + "line-color": "hsl(0, 0%, 23%)", + "line-opacity": 1, + "line-width": { + "base": 1.1, + "stops": [ + [ + 3, + 1 + ], + [ + 22, + 20 + ] + ] + } + } + }, + { + "id": "boundary_country_z5-", + "type": "line", + "metadata": { + "mapbox:group": "a14c9607bc7954ba1df7205bf660433f" + }, + "source": "openmaptiles", + "source-layer": "boundary", + "minzoom": 5, + "filter": [ + "==", + "admin_level", + 2 + ], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-blur": { + "base": 1, + "stops": [ + [ + 0, + 0.4 + ], + [ + 22, + 4 + ] + ] + }, + "line-color": "hsl(0, 0%, 23%)", + "line-opacity": 1, + "line-width": { + "base": 1.1, + "stops": [ + [ + 3, + 1 + ], + [ + 22, + 20 + ] + ] + } + } + }, + { + "id": "place_other", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 14, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "in", + "class", + "hamlet", + "isolated_dwelling", + "neighbourhood" + ] + ], + "layout": { + "text-anchor": "center", + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": [ + "Noto Sans Regular", + "Metropolis Regular" + ], + "text-justify": "center", + "text-offset": [ + 0.5, + 0 + ], + "text-size": 10, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "text-color": "rgb(101,101,101)", + "text-halo-blur": 1, + "text-halo-color": "rgba(0,0,0,0.7)", + "text-halo-width": 1 + } + }, + { + "id": "place_suburb", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 15, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "==", + "class", + "suburb" + ] + ], + "layout": { + "text-anchor": "center", + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": [ + "Noto Sans Regular", + "Metropolis Regular" + ], + "text-justify": "center", + "text-offset": [ + 0.5, + 0 + ], + "text-size": 10, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "text-color": "rgb(101,101,101)", + "text-halo-blur": 1, + "text-halo-color": "rgba(0,0,0,0.7)", + "text-halo-width": 1 + } + }, + { + "id": "place_village", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 14, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "==", + "class", + "village" + ] + ], + "layout": { + "icon-size": 0.4, + "text-anchor": "left", + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": [ + "Noto Sans Regular", + "Metropolis Regular" + ], + "text-justify": "left", + "text-offset": [ + 0.5, + 0.2 + ], + "text-size": 10, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "icon-opacity": 0.7, + "text-color": "rgb(101,101,101)", + "text-halo-blur": 1, + "text-halo-color": "rgba(0,0,0,0.7)", + "text-halo-width": 1 + } + }, + { + "id": "place_town", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 15, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "==", + "class", + "town" + ] + ], + "layout": { + "icon-image": { + "base": 1, + "stops": [ + [ + 0, + "circle-11" + ], + [ + 9, + "" + ] + ] + }, + "icon-size": 0.4, + "text-anchor": { + "base": 1, + "stops": [ + [ + 0, + "left" + ], + [ + 8, + "center" + ] + ] + }, + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": [ + "Noto Sans Regular", + "Metropolis Regular" + ], + "text-justify": "left", + "text-offset": [ + 0.5, + 0.2 + ], + "text-size": 10, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "icon-opacity": 0.7, + "text-color": "rgb(101,101,101)", + "text-halo-blur": 1, + "text-halo-color": "rgba(0,0,0,0.7)", + "text-halo-width": 1 + } + }, + { + "id": "place_city", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 14, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "==", + "class", + "city" + ], + [ + ">", + "rank", + 3 + ] + ], + "layout": { + "icon-image": { + "base": 1, + "stops": [ + [ + 0, + "circle-11" + ], + [ + 9, + "" + ] + ] + }, + "icon-size": 0.4, + "text-anchor": { + "base": 1, + "stops": [ + [ + 0, + "left" + ], + [ + 8, + "center" + ] + ] + }, + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": [ + "Noto Sans Regular", + "Metropolis Regular" + ], + "text-justify": "left", + "text-offset": [ + 0.5, + 0.2 + ], + "text-size": 10, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "icon-opacity": 0.7, + "text-color": "rgb(101,101,101)", + "text-halo-blur": 1, + "text-halo-color": "rgba(0,0,0,0.7)", + "text-halo-width": 1 + } + }, + { + "id": "place_city_large", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 12, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "<=", + "rank", + 3 + ], + [ + "==", + "class", + "city" + ] + ], + "layout": { + "icon-image": { + "base": 1, + "stops": [ + [ + 0, + "circle-11" + ], + [ + 9, + "" + ] + ] + }, + "icon-size": 0.4, + "text-anchor": { + "base": 1, + "stops": [ + [ + 0, + "left" + ], + [ + 8, + "center" + ] + ] + }, + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": [ + "Noto Sans Regular", + "Metropolis Regular" + ], + "text-justify": "left", + "text-offset": [ + 0.5, + 0.2 + ], + "text-size": 14, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "icon-opacity": 0.7, + "text-color": "rgb(101,101,101)", + "text-halo-blur": 1, + "text-halo-color": "rgba(0,0,0,0.7)", + "text-halo-width": 1 + } + }, + { + "id": "place_state", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 12, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "==", + "class", + "state" + ] + ], + "layout": { + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": [ + "Noto Sans Regular", + "Metropolis Regular" + ], + "text-size": 10, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "text-color": "rgb(101,101,101)", + "text-halo-blur": 1, + "text-halo-color": "rgba(0,0,0,0.7)", + "text-halo-width": 1 + } + }, + { + "id": "place_country_other", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 8, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "==", + "class", + "country" + ], + [ + "!has", + "iso_a2" + ] + ], + "layout": { + "text-field": "{name:latin}", + "text-font": [ + "Noto Sans Italic", + "Metropolis Light Italic" + ], + "text-size": { + "base": 1, + "stops": [ + [ + 0, + 9 + ], + [ + 1, + 11 + ] + ] + }, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "text-color": "rgb(101,101,101)", + "text-halo-color": "rgba(0,0,0,0.7)", + "text-halo-width": 1.4 + } + }, + { + "id": "place_country_minor", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 8, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "==", + "class", + "country" + ], + [ + ">=", + "rank", + 2 + ], + [ + "has", + "iso_a2" + ] + ], + "layout": { + "text-field": "{name:latin}", + "text-font": [ + "Noto Sans Regular", + "Metropolis Regular" + ], + "text-size": { + "base": 1, + "stops": [ + [ + 0, + 10 + ], + [ + 6, + 12 + ] + ] + }, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "text-color": "rgb(101,101,101)", + "text-halo-color": "rgba(0,0,0,0.7)", + "text-halo-width": 1.4 + } + }, + { + "id": "place_country_major", + "type": "symbol", + "metadata": { + "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" + }, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 6, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "<=", + "rank", + 1 + ], + [ + "==", + "class", + "country" + ], + [ + "has", + "iso_a2" + ] + ], + "layout": { + "text-anchor": "center", + "text-field": "{name:latin}", + "text-font": [ + "Noto Sans Regular", + "Metropolis Regular" + ], + "text-size": { + "base": 1.4, + "stops": [ + [ + 0, + 10 + ], + [ + 3, + 12 + ], + [ + 4, + 14 + ] + ] + }, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "text-color": "rgb(101,101,101)", + "text-halo-color": "rgba(0,0,0,0.7)", + "text-halo-width": 1.4 + } + } + ] +} \ No newline at end of file diff --git a/frontend/scripts/update-map-palette.ts b/frontend/scripts/update-map-palette.ts new file mode 100644 index 0000000..d5037f8 --- /dev/null +++ b/frontend/scripts/update-map-palette.ts @@ -0,0 +1,584 @@ +#!/usr/bin/env node +/** + * به‌روزرسانی یک‌بارهٔ پالت `style.json` نقشهٔ آفلاین روی MinIO. + * + * ترتیب کار (مهم): اول نسخهٔ فعلی خوانده و «پشتیبان» می‌شود — هم روی دیسک + * (style.backup.json) و هم روی MinIO (maps/style.backup.json) — و تنها پس از + * موفقیت پشتیبان، پالت تازه نوشته و آپلود می‌شود. + * + * چرا از S3 API و نه فایل روی دیسک؟ شیءهای MinIO با فرمت داخلی xl.meta ذخیره + * می‌شوند، نه JSON خام؛ پس تنها راه درست به‌روزرسانی، PUT روی همان API است. + * + * اجرا (از پوشهٔ frontend) — هر کار با پرچم مشخص، پیش‌فرض هیچ تغییری نمی‌دهد: + * node --experimental-strip-types scripts/update-map-palette.ts --fix-glyphs + * node --experimental-strip-types scripts/update-map-palette.ts --palette + * node --experimental-strip-types scripts/update-map-palette.ts --restore + * ... [--dry-run] برای دیدن گزارش بدون نوشتن روی MinIO + * + * در همهٔ حالت‌ها (جز restore) ترتیب امن رعایت می‌شود: خواندن و اعتبارسنجی → + * پشتیبان روی دیسک و MinIO → تغییر → اعتبارسنجی دوباره → آپلود. + * + * کلیدهای MinIO از فایل .env ریشهٔ پروژه خوانده می‌شوند (MINIO_ROOT_USER / + * MINIO_ROOT_PASSWORD) و در نبودشان از متغیرهای محیطی همان نام‌ها. + */ + +import { createHash, createHmac } from 'node:crypto' +import { mkdir, readFile, writeFile } from 'node:fs/promises' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +/* ------------------------------------------------------------------ */ +/* پالت تازه */ +/* ------------------------------------------------------------------ */ + +/** نگاشت دسته‌ها به رنگ‌های تازهٔ برند — تنها منبع حقیقت این اسکریپت */ +const PALETTE = { + background: '#181512', + urban: '#27221c', + highway: '#6a5544', + street: '#4a3a2d', + alley: '#352c25', + water: '#172c32', + parks: '#20312b', + nature: '#314238', + mainText: '#ead9c6', + subText: '#a99584', + borders: '#3e362e', +} as const + +type Category = keyof typeof PALETTE + +/* ------------------------------------------------------------------ */ +/* قواعد دسته‌بندی لایه‌ها */ +/* ------------------------------------------------------------------ */ + +type Layer = { + id: string + type: string + 'source-layer'?: string + paint?: Record + layout?: Record +} +type StyleDocument = { + version?: number + layers?: Layer[] + sources?: Record + /** الگوی نشانی گلیف‌ها — باید به پوشهٔ v2.0 باکت اشاره کند */ + glyphs?: string +} + +/** پوشهٔ فونت‌های آفلاین در باکت MinIO */ +const GLYPHS_FOLDER = 'v2.0' +/** الگوی استاندارد گلیف — جانگهدارها دست‌نخورده می‌مانند */ +const GLYPH_TEMPLATE = '{fontstack}/{range}.pbf' + +/** + * اصلاح مسیر گلیف‌ها: هر شکل نشانی (خالی، نسبی، یا با پوشهٔ اشتباه) به + * «v2.0/{fontstack}/{range}.pbf» تبدیل می‌شود تا با ساختار پوشه‌های باکت + * بخواند. تابع خالص و idempotent است؛ اگر مسیر از قبل درست باشد چیز تغییر نمی‌کند. + */ +function fixGlyphsPath(style: StyleDocument): { from: string; to: string } | null { + const current = (style.glyphs ?? '').trim() + const marker = current.indexOf('{fontstack}') + const next = `${GLYPHS_FOLDER}/${marker >= 0 ? current.slice(marker) : GLYPH_TEMPLATE}` + if (next === current) return null + style.glyphs = next + return { from: current || '(خالی)', to: next } +} + +/** آیا مقدار text-field به یک فیلد «نام» اشاره می‌کند؟ (name، name_en، name:latin و…) */ +const NAME_REFERENCE = /name(:[a-z-]+)?/i + +/** + * فونت‌های محلی با پوشش فارسی — با اجرای «--audit-fonts» قابل بازبینی است. + * تنها پوشه‌های Noto Sans در باکت گلیف عربی/فارسی دارند؛ Metropolis و بقیه + * فقط لاتین‌اند. + */ +const PERSIAN_CAPABLE_FONTS = ['Noto Sans Regular', 'Noto Sans Italic', 'Noto Sans Bold'] + +/** + * بازگردانی متن برچسب‌ها به قالب دوزبانهٔ اصلی، به‌تفکیک لایه. + * (لاتین خط اول، فارسی خط دوم — یا کنار هم برای نام راه‌ها) + */ +const TEXT_FIELD_RESTORE: Record = { + water_name: '{name:latin}\n{name:nonlatin}', + place_other: '{name:latin}\n{name:nonlatin}', + place_suburb: '{name:latin}\n{name:nonlatin}', + place_village: '{name:latin}\n{name:nonlatin}', + place_town: '{name:latin}\n{name:nonlatin}', + place_city: '{name:latin}\n{name:nonlatin}', + place_city_large: '{name:latin}\n{name:nonlatin}', + place_state: '{name:latin}\n{name:nonlatin}', + highway_name_other: '{name:latin} {name:nonlatin}', + place_country_other: '{name:latin}', + place_country_minor: '{name:latin}', + place_country_major: '{name:latin}', +} + +/** قالب پیش‌فرض برای هر لایهٔ برچسبِ دیگری که بعداً اضافه شود */ +const DEFAULT_BILINGUAL_FIELD = '{name:latin}\n{name:nonlatin}' + +function referencesName(value: unknown): boolean { + if (typeof value === 'string') return NAME_REFERENCE.test(value) + if (Array.isArray(value)) return value.some(referencesName) + if (value && typeof value === 'object') return Object.values(value).some(referencesName) + return false +} + +/** فونت فارسی متناسب با پشتهٔ همان لایه — ایتالیک بماند ایتالیک */ +function persianFontFor(stack: string[]): string { + const local = stack.find((font) => PERSIAN_CAPABLE_FONTS.includes(font)) + if (local) return local + return stack.some((font) => /italic/i.test(font)) ? 'Noto Sans Italic' : 'Noto Sans Regular' +} + +type LabelChange = { id: string; field?: { from: string; to: string }; font: { from: string; to: string } } + +/** + * آماده‌سازی برچسب‌ها برای فارسی: + * ۱) متن به قالب دوزبانهٔ اصلی برمی‌گردد (لاتین + nonlatin که برای دادهٔ ایران فارسی است). + * ۲) پشتهٔ فونت بازچینی می‌شود تا فونت دارای گلیف فارسی (Noto Sans محلی) اول بیفتد؛ + * چون مپ‌لیبر برای هر پشته فقط یک فایل گلیف می‌گیرد، همان فونت اول باید هر دو خط + * را پوشش دهد و Noto Sans لاتین را هم دارد. + */ +function fixLabels(style: StyleDocument): { changed: LabelChange[]; skipped: string[] } { + const changed: LabelChange[] = [] + const skipped: string[] = [] + + for (const layer of style.layers ?? []) { + const layout = layer.layout + if (layer.type !== 'symbol' || !layout) continue + + const stack = Array.isArray(layout['text-font']) ? (layout['text-font'] as unknown[]).map(String) : [] + if (!stack.length) continue + + const persianFont = persianFontFor(stack) + const restacked = [persianFont, ...stack.filter((font) => font !== persianFont)] + const fontChange = { from: stack.join(', '), to: restacked.join(', ') } + + if (!('text-field' in layout)) { + // لایهٔ برچسب بدون فیلد نام (مثل شمارهٔ راه) — فقط پشتهٔ فونت اصلاح می‌شود + layout['text-font'] = restacked + changed.push({ id: layer.id, font: fontChange }) + continue + } + + if (!referencesName(layout['text-field'])) { + layout['text-font'] = restacked + skipped.push(layer.id) + continue + } + + const from = JSON.stringify(layout['text-field']).slice(0, 48) + const to = TEXT_FIELD_RESTORE[layer.id] ?? DEFAULT_BILINGUAL_FIELD + layout['text-field'] = to + layout['text-font'] = restacked + changed.push({ id: layer.id, field: { from, to }, font: fontChange }) + } + + return { changed, skipped } +} + +/** + * بررسی فونت‌ها: هر فونتی که در text-font لایه‌های برچسب استفاده شده باید در + * پوشهٔ v2.0 باکت موجود باشد — هیچ نشانی بیرونی و هیچ فونت غایبی مجاز نیست. + */ +async function verifyFonts(style: StyleDocument): Promise { + const fonts = new Set() + for (const layer of style.layers ?? []) { + if (layer.type !== 'symbol') continue + const stack = layer.layout?.['text-font'] + if (Array.isArray(stack)) for (const font of stack) if (typeof font === 'string') fonts.add(font) + } + + if (!fonts.size) { + console.log('• فونتی در لایه‌های برچسب پیدا نشد') + return + } + + console.log(`\nبررسی ${fonts.size} فونت در پوشهٔ ${GLYPHS_FOLDER}:`) + const missing: string[] = [] + for (const font of [...fonts].sort()) { + const url = new URL(`/maps/${GLYPHS_FOLDER}/${encodeURIComponent(font)}/0-255.pbf`, ENDPOINT) + const ok = (await fetch(url, { method: 'HEAD' })).ok + if (!ok) missing.push(font) + console.log(` ${ok ? '✓' : '✗'} ${font}`) + } + + console.log( + missing.length + ? `✗ فونت‌های ناموجود در ${GLYPHS_FOLDER}: ${missing.join(', ')}` + : `✓ همهٔ فونت‌ها در ${GLYPHS_FOLDER} باکت موجودند`, + ) +} + +/* ------------------------------------------------------------------ */ +/* بررسی پوشش فونت‌های محلی (کدام فونت فارسی را رندر می‌کند؟) */ +/* ------------------------------------------------------------------ */ + +/** بازه‌های کلیدی گلیف: لاتین پایه، عربی/فارسی، فرم‌های نمایشی عربی */ +const COVERAGE_RANGES = [ + { label: 'لاتین', range: '0-255' }, + { label: 'فارسی', range: '1536-1791' }, + { label: 'فرم فارسی', range: '65024-65279' }, +] as const + +/** فهرست پوشه‌های فونت داخل v2.0 باکت */ +async function listFontFolders(): Promise { + const response = await fetch( + new URL(`/maps/?list-type=2&prefix=${GLYPHS_FOLDER}/&delimiter=/&max-keys=1000`, ENDPOINT), + ) + const xml = await response.text() + const pattern = new RegExp(`${GLYPHS_FOLDER}/([^<]+)/`, 'g') + return [...xml.matchAll(pattern)].map((match) => match[1]) +} + +/** وضعیت یک بازهٔ گلیف برای یک فونت: «نیست» / «خالی» / حجم واقعی */ +async function rangeCoverage(font: string, range: string): Promise { + const url = new URL(`/maps/${GLYPHS_FOLDER}/${encodeURIComponent(font)}/${range}.pbf`, ENDPOINT) + const response = await fetch(url, { method: 'HEAD' }) + if (!response.ok) return 'نیست' + const bytes = Number(response.headers.get('content-length') ?? 0) + // بازهٔ بدون گلیف فقط سرصفحهٔ پروتوباف است (چند ده بایت) + return bytes > 2000 ? `${Math.round(bytes / 1024)}KB` : 'خالی' +} + +/** گزارش پوشش خط‌ها برای همهٔ فونت‌های باکت — ورودی تصمیم انتخاب فونت برچسب‌ها */ +async function auditFonts(): Promise { + const fonts = await listFontFolders() + console.log(`${fonts.length} پوشهٔ فونت در ${GLYPHS_FOLDER}:\n`) + for (const font of fonts) { + const cells: string[] = [] + for (const { label, range } of COVERAGE_RANGES) cells.push(`${label}=${await rangeCoverage(font, range)}`) + console.log(` ${font.padEnd(26)} ${cells.join(' ')}`) + } + console.log('\nفونتی که «فارسی» و «فرم فارسی» آن خالی/نیست نباشد، برچسب فارسی را رندر می‌کند.') +} + +/** + * اعتبارسنجی ساختار سبک — پیش و پس از تغییر اجرا می‌شود تا مطمئن شویم سند + * سالم است و اسکیمای سبک (نسخه، لایه‌ها، منابع) نشکسته است. + */ +function validateStyle(style: StyleDocument, label: string): void { + const problems: string[] = [] + if (style.version !== 8) problems.push(`version باید ۸ باشد (مقدار فعلی: ${String(style.version)})`) + if (!Array.isArray(style.layers) || style.layers.length === 0) problems.push('layers باید آرایهٔ ناتهی باشد') + else { + const broken = style.layers.filter((layer) => !layer || typeof layer.id !== 'string' || typeof layer.type !== 'string') + if (broken.length) problems.push(`${broken.length} لایه بدون id/type`) + } + if (!style.sources || typeof style.sources !== 'object' || Array.isArray(style.sources)) { + problems.push('sources باید یک شیء باشد') + } + + console.log( + problems.length + ? `✗ اعتبارسنجی ${label}: ${problems.join(' | ')}` + : `✓ اعتبارسنجی ${label}: ساختار سالم است (${style.layers?.length ?? 0} لایه، ${Object.keys(style.sources ?? {}).length} منبع)`, + ) + if (problems.length) throw new Error(`ساختار سبک نامعتبر است (${label})`) +} + +type Rule = { + category: Category + /** با این الگو روی شناسهٔ لایه تطبیق داده می‌شود */ + test: RegExp + /** کدام ویژگی‌های paint رنگ‌آمیزی شوند */ + properties: string[] +} + +/** + * جدول قواعد — به ترتیب بررسی می‌شود و نخستین تطبیق برنده است. + * رنگ‌های داخل عبارت‌ها (interpolate/match) هم بازنویسی می‌شوند تا پالت + * یکدست بماند. + */ +const RULES: Rule[] = [ + { category: 'background', test: /^background$/, properties: ['background-color'] }, + + // آب: خود لایه‌های آب و راه‌های آبی (برچسب‌ها قاعدهٔ جدا دارند) + { category: 'water', test: /^(water|waterway)(_|$)/, properties: ['fill-color', 'line-color'] }, + + // پارک‌ها پیش از طبیعت بررسی می‌شوند تا چمن/پارک زیر «طبیعت» نرود + { category: 'parks', test: /^(park|landcover_grass|landuse_(park|cemetery|pitch|grass))/, properties: ['fill-color', 'line-color'] }, + + // طبیعت: پوشش زمینِ جنگلی/علف‌زارِ بیرون از شهر + { category: 'nature', test: /^landcover_/, properties: ['fill-color', 'line-color'] }, + + // کاربری شهری و ساختمان‌ها + { category: 'urban', test: /^landuse_/, properties: ['fill-color', 'line-color'] }, + { category: 'urban', test: /^building/, properties: ['fill-color'] }, + + // سه ردهٔ راه — بزرگراه، خیابان، کوچه/ریل/باند فرودگاه + { category: 'highway', test: /^highway_motorway/, properties: ['line-color', 'fill-color'] }, + { category: 'street', test: /^(highway_(major|minor)|road_)/, properties: ['line-color', 'fill-color'] }, + { category: 'alley', test: /^(highway_(path|track)|railway|aeroway)/, properties: ['line-color', 'fill-color'] }, + + { category: 'borders', test: /^boundary/, properties: ['line-color', 'fill-color'] }, + + // برچسب‌های اصلی: شهر/شهرک و نام بزرگراه‌ها + { category: 'mainText', test: /^(place_(city|town)|highway_name_motorway)/, properties: ['text-color'] }, + + // بقیهٔ برچسب‌ها (روستا، محله، نام خیابان، نام آب، نقاط) — متن فرعی + { category: 'subText', test: /(_name|^place_|_label)/, properties: ['text-color'] }, +] + +/** ویژگی‌های فرعی که همیشه با رنگ پس‌زمینه هماهنگ می‌شوند تا خوانایی بماند */ +const HALO_PROPERTIES = ['text-halo-color', 'icon-halo-color'] +/** ویژگی‌های آیکون برچسب‌ها که با متن هم‌رنگ می‌شوند */ +const ICON_PROPERTIES = ['icon-color'] + +const COLOR_PATTERN = /^(#|rgb|hsl)/i +const isColor = (value: unknown): value is string => typeof value === 'string' && COLOR_PATTERN.test(value.trim()) + +/* ------------------------------------------------------------------ */ +/* بازنویسی رنگ‌ها (توابع خالص) */ +/* ------------------------------------------------------------------ */ + +/** هر رشتهٔ رنگی داخل یک مقدار paint — حتی داخل عبارت‌ها — را با رنگ تازه عوض می‌کند */ +function paintColor(value: unknown, color: string, stats: { replaced: number; seen: Set }): unknown { + if (isColor(value)) { + stats.seen.add(value.trim()) + stats.replaced += 1 + return color + } + if (Array.isArray(value)) return value.map((item) => paintColor(item, color, stats)) + if (value && typeof value === 'object') { + const result: Record = {} + for (const [key, inner] of Object.entries(value)) result[key] = paintColor(inner, color, stats) + return result + } + return value +} + +type LayerReport = { id: string; category: Category | null; replaced: number; colors: string[] } + +/** یک لایه را بر پایهٔ قواعد رنگ‌آمیزی می‌کند و گزارش تغییرها را برمی‌گرداند */ +function recolorLayer(layer: Layer): LayerReport { + const rule = RULES.find((candidate) => candidate.test.test(layer.id)) + const stats = { replaced: 0, seen: new Set() } + + if (!layer.paint) return { id: layer.id, category: rule?.category ?? null, replaced: 0, colors: [] } + + if (rule) { + for (const property of rule.properties) { + if (!(property in layer.paint)) continue + layer.paint[property] = paintColor(layer.paint[property], PALETTE[rule.category], stats) + } + } + + // لایه‌های برچسب: هاله و آیکون‌ها با پالت هماهنگ می‌شوند + if (layer.type === 'symbol') { + const textColor = rule?.category === 'mainText' || rule?.category === 'subText' + ? PALETTE[rule.category] + : PALETTE.subText + if (rule?.category !== 'mainText' && rule?.category !== 'subText' && 'text-color' in layer.paint) { + layer.paint['text-color'] = paintColor(layer.paint['text-color'], textColor, stats) + } + for (const property of [...HALO_PROPERTIES, ...ICON_PROPERTIES]) { + if (!(property in layer.paint)) continue + const target = property === 'icon-color' ? textColor : PALETTE.background + layer.paint[property] = paintColor(layer.paint[property], target, stats) + } + } + + // لبهٔ ساختمان‌ها با رنگ مرزها هم‌خانواده می‌شود + if (layer.paint['fill-outline-color'] && /^building/.test(layer.id)) { + layer.paint['fill-outline-color'] = paintColor(layer.paint['fill-outline-color'], PALETTE.borders, stats) + } + + return { id: layer.id, category: rule?.category ?? null, replaced: stats.replaced, colors: [...stats.seen] } +} + +/** کل سند سبک را بازنویسی می‌کند (بدون دست‌زدن به ساختار و ویژگی‌های غیررنگی) */ +function recolorStyle(style: StyleDocument): LayerReport[] { + const layers = style.layers ?? [] + return layers.map(recolorLayer) +} + +/** گزارش فشردهٔ تغییرهای پالت — به‌تفکیک دسته و لایه‌های دست‌نخورده */ +function reportPalette(reports: LayerReport[]): void { + const byCategory = new Map() + for (const report of reports) { + if (!report.replaced) continue + const category = report.category ?? 'بدون‌دسته' + byCategory.set(category, (byCategory.get(category) ?? 0) + report.replaced) + } + + console.log('\nگزارش تغییرها:') + for (const [category, count] of [...byCategory].sort((a, b) => b[1] - a[1])) { + console.log(` ${category.padEnd(12)} ${String(count).padStart(3)} مقدار رنگ`) + } + + const untouched = reports.filter((report) => !report.replaced) + if (untouched.length) { + console.log(`\nلایه‌های بدون تغییر (${untouched.length}): ${untouched.map((report) => report.id).join(', ')}`) + } +} + +/* ------------------------------------------------------------------ */ +/* دسترسی به MinIO (S3 Signature V4 — بدون وابستگی تازه) */ +/* ------------------------------------------------------------------ */ + +type Credentials = { user: string; secret: string } + +async function readCredentials(): Promise { + const envPath = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '.env') + const fromFile: Record = {} + try { + for (const line of (await readFile(envPath, 'utf8')).split(/\r?\n/)) { + const match = line.match(/^([A-Z0-9_]+)=(.*)$/) + if (match) fromFile[match[1]] = match[2].trim() + } + } catch { + /* فایل .env اختیاری است — متغیرهای محیطی جایگزین می‌شوند */ + } + + const user = process.env.MINIO_ROOT_USER || fromFile.MINIO_ROOT_USER || 'aramesh' + const secret = process.env.MINIO_ROOT_PASSWORD || fromFile.MINIO_ROOT_PASSWORD + if (!secret) throw new Error('MINIO_ROOT_PASSWORD پیدا نشد (نه در .env و نه در متغیرهای محیطی)') + return { user, secret } +} + +const sha256 = (value: string | Buffer) => createHash('sha256').update(value).digest('hex') +const hmac = (key: string | Buffer, value: string) => createHmac('sha256', key).update(value).digest() +const hmacHex = (key: Buffer, value: string) => createHmac('sha256', key).update(value).digest('hex') + +/** PUT امضاشدهٔ S3 — همان چیزی که کنسول MinIO و پنل ادمین هم انجام می‌دهند */ +async function putObject(endpoint: string, objectPath: string, body: string, credentials: Credentials): Promise { + const url = new URL(objectPath, endpoint) + const amzDate = new Date().toISOString().replace(/[:-]|\.\d{3}/g, '') + const dateStamp = amzDate.slice(0, 8) + const payloadHash = sha256(body) + const canonicalHeaders = `host:${url.host}\nx-amz-content-sha256:${payloadHash}\nx-amz-date:${amzDate}\n` + const signedHeaders = 'host;x-amz-content-sha256;x-amz-date' + const canonicalRequest = ['PUT', url.pathname, '', canonicalHeaders, signedHeaders, payloadHash].join('\n') + const scope = `${dateStamp}/us-east-1/s3/aws4_request` + const stringToSign = ['AWS4-HMAC-SHA256', amzDate, scope, sha256(canonicalRequest)].join('\n') + const signingKey = hmac(hmac(hmac(hmac(`AWS4${credentials.secret}`, dateStamp), 'us-east-1'), 's3'), 'aws4_request') + const authorization = + `AWS4-HMAC-SHA256 Credential=${credentials.user}/${scope}, ` + + `SignedHeaders=${signedHeaders}, Signature=${hmacHex(signingKey, stringToSign)}` + + const response = await fetch(url, { + method: 'PUT', + headers: { + Authorization: authorization, + 'x-amz-content-sha256': payloadHash, + 'x-amz-date': amzDate, + 'Content-Type': 'application/json', + }, + body, + }) + if (!response.ok) throw new Error(`PUT ${objectPath} → HTTP ${response.status} ${await response.text()}`) +} + +/* ------------------------------------------------------------------ */ +/* اجرا */ +/* ------------------------------------------------------------------ */ + +const ENDPOINT = process.env.MAP_ENDPOINT ?? 'http://localhost:19000' +const STYLE_PATH = '/maps/style.json' +const BACKUP_PATH = '/maps/style.backup.json' +const OUTPUT_DIR = join(dirname(fileURLToPath(import.meta.url)), 'map-palette') +const LOCAL_BACKUP = join(OUTPUT_DIR, 'style.backup.json') + +/** بازگردانی: پشتیبان دقیقاً به‌جای style.json نوشته می‌شود */ +async function restore(): Promise { + const backup = await readFile(LOCAL_BACKUP, 'utf8') + validateStyle(JSON.parse(backup) as StyleDocument, 'نسخهٔ پشتیبان') + await putObject(ENDPOINT, STYLE_PATH, backup, await readCredentials()) + + const restored = await (await fetch(new URL(STYLE_PATH, ENDPOINT))).text() + console.log( + restored === backup + ? '✓ style.json دقیقاً به نسخهٔ پشتیبان برگشت' + : '✗ محتوای بازگردانده‌شده با پشتیبان یکسان نیست — دستی بررسی کنید', + ) +} + +const ACTIONS = ['--audit-fonts', '--fix-labels', '--fix-glyphs', '--palette', '--restore'] as const + +async function main(): Promise { + const dryRun = process.argv.includes('--dry-run') + const action = ACTIONS.find((flag) => process.argv.includes(flag)) + + // پیش‌فرض امن: بدون پرچم هیچ نوشتنی انجام نمی‌شود + if (!action) { + console.log( + 'کاری انجام نشد — یکی از پرچم‌ها را بدهید:\n' + + ' --audit-fonts گزارش پوشش خط‌های هر فونت در v2.0 (فقط خواندن)\n' + + ' --fix-labels برچسب‌های دوزبانه + پشتهٔ فونت محلی\n' + + ' --fix-glyphs اصلاح مسیر گلیف‌ها به v2.0\n' + + ' --palette اعمال پالت تازه\n' + + ' --restore بازگردانی از پشتیبان\n' + + ' [--dry-run] فقط گزارش، بدون نوشتن روی MinIO', + ) + return + } + + if (action === '--audit-fonts') { + await auditFonts() + return + } + + if (action === '--restore') { + await restore() + return + } + + // ۱) خواندن سبک فعلی + const response = await fetch(new URL(STYLE_PATH, ENDPOINT)) + if (!response.ok) throw new Error(`GET ${STYLE_PATH} → HTTP ${response.status}`) + const original = await response.text() + const style = JSON.parse(original) as StyleDocument + console.log(`سبک فعلی خوانده شد — ${(original.length / 1024).toFixed(1)} کیلوبایت`) + validateStyle(style, 'نسخهٔ فعلی') + + // ۲) پشتیبان‌گیری، پیش از هر تغییر + await mkdir(OUTPUT_DIR, { recursive: true }) + await writeFile(LOCAL_BACKUP, original, 'utf8') + console.log(`پشتیبان روی دیسک: scripts/map-palette/style.backup.json`) + if (!dryRun) { + await putObject(ENDPOINT, BACKUP_PATH, original, await readCredentials()) + console.log(`پشتیبان روی MinIO: ${BACKUP_PATH}`) + } + + // ۳) تغییر درخواستی، سپس اعتبارسنجی دوباره + if (action === '--fix-labels') { + const { changed, skipped } = fixLabels(style) + console.log(`\nلایه‌های برچسب اصلاح‌شده (${changed.length}):`) + for (const layer of changed) { + if (layer.field) console.log(` ${layer.id}\n متن: ${layer.field.from} → ${JSON.stringify(layer.field.to)}`) + console.log(` فونت: ${layer.font.from} → ${layer.font.to}`) + } + if (skipped.length) { + console.log(`\nلایه‌های بدون فیلد نام (فقط فونت اصلاح شد): ${skipped.join(', ')}`) + } + await verifyFonts(style) + } else if (action === '--fix-glyphs') { + const change = fixGlyphsPath(style) + console.log( + change ? `\nمسیر گلیف‌ها: ${change.from} → ${change.to}` : '\nمسیر گلیف‌ها از قبل به v2.0 اشاره می‌کند', + ) + await verifyFonts(style) + } else { + reportPalette(recolorStyle(style)) + } + validateStyle(style, 'نسخهٔ تازه') + + // ۴) نوشتن و آپلود نسخهٔ تازه + const updated = JSON.stringify(style, null, 2) + await writeFile(join(OUTPUT_DIR, 'style.json'), updated, 'utf8') + if (dryRun) { + console.log('\nحالت آزمایشی — هیچ چیزی روی MinIO نوشته نشد.') + return + } + + await putObject(ENDPOINT, STYLE_PATH, updated, await readCredentials()) + console.log(`\nسبک تازه روی MinIO نوشته شد: ${STYLE_PATH}`) +} + +main().catch((error: unknown) => { + console.error('\nخطا:', error instanceof Error ? error.message : error) + process.exitCode = 1 +}) diff --git a/frontend/src/admin/AdminContext.tsx b/frontend/src/admin/AdminContext.tsx index d70befc..81547e5 100644 --- a/frontend/src/admin/AdminContext.tsx +++ b/frontend/src/admin/AdminContext.tsx @@ -3,7 +3,7 @@ import type { ReactNode } from 'react' import Swal from 'sweetalert2' import type { SiteData } from '../api' import { useSiteData } from '../api' -import { adminApi, getToken, HttpError, setToken } from './adminClient' +import { adminApi, getToken, HttpError, setToken, AUTH_CHANGE_EVENT } from './adminClient' import { ENTITIES, isTranslatable } from './entities' export type SaveStatus = 'idle' | 'saving' | 'error' @@ -129,6 +129,18 @@ export function AdminProvider({ children }: { children: ReactNode }) { return () => document.removeEventListener('click', onClick, true) }, [isEditMode]) + // توکن بیرون از این provider هم پاک می‌شود (پاسخ 401 بارگذاری سایت، خروج در زبانه + // دیگر) — وضعیت ورود همگام می‌ماند و نوار ادمین برای نشست مرده باقی نمی‌ماند + useEffect(() => { + const syncAuth = () => setIsAuthenticated(!!getToken()) + window.addEventListener(AUTH_CHANGE_EVENT, syncAuth) + window.addEventListener('storage', syncAuth) + return () => { + window.removeEventListener(AUTH_CHANGE_EVENT, syncAuth) + window.removeEventListener('storage', syncAuth) + } + }, []) + // در حین ذخیره‌سازی، از بسته‌شدن تصادفی صفحه جلوگیری می‌شود useEffect(() => { if (saveStatus !== 'saving') return diff --git a/frontend/src/admin/adminClient.ts b/frontend/src/admin/adminClient.ts index 1461b19..ee58c8e 100644 --- a/frontend/src/admin/adminClient.ts +++ b/frontend/src/admin/adminClient.ts @@ -3,10 +3,14 @@ import { API_URL } from '../config' const TOKEN_KEY = 'aramesh_admin_token' +/** رویداد تغییر وضعیت ورود — لایه‌های دیگر (مثل نوار ادمین) با آن همگام می‌مانند */ +export const AUTH_CHANGE_EVENT = 'aramesh:auth-change' + export const getToken = (): string | null => localStorage.getItem(TOKEN_KEY) export const setToken = (token: string | null): void => { if (token) localStorage.setItem(TOKEN_KEY, token) else localStorage.removeItem(TOKEN_KEY) + window.dispatchEvent(new CustomEvent(AUTH_CHANGE_EVENT)) } export class HttpError extends Error { diff --git a/frontend/src/api.tsx b/frontend/src/api.tsx index 4739467..a98f447 100644 --- a/frontend/src/api.tsx +++ b/frontend/src/api.tsx @@ -2,7 +2,7 @@ import { createContext, useCallback, useContext, useEffect, useState } from 'rea import type { ReactNode } from 'react' import type { Article, Project, Service } from './data/site' import { API_URL } from './config' -import { getToken } from './admin/adminClient' +import { getToken, setToken } from './admin/adminClient' import { localizeSite } from './i18n/localize' import { useLang } from './i18n/lang' @@ -230,10 +230,16 @@ interface SiteContextValue { const SiteContext = createContext({ data: null, error: false, + // پیش‌فرض‌های بی‌اثر — مصرف‌کننده بیرون از Provider هم بدون خطا رندر می‌شود + // eslint-disable-next-line @typescript-eslint/no-empty-function reload: () => {}, + // eslint-disable-next-line @typescript-eslint/no-empty-function updateSite: () => {}, }) +/** سقف انتظار داده سایت — سرور بی‌پاسخ یعنی صفحه خطا با دکمه تلاش دوباره، نه انتظار بی‌پایان */ +const SITE_TIMEOUT_MS = 15_000 + export function SiteProvider({ children }: { children: ReactNode }) { const [data, setData] = useState(null) const [error, setError] = useState(false) @@ -241,7 +247,9 @@ export function SiteProvider({ children }: { children: ReactNode }) { const load = useCallback(() => { let cancelled = false setError(false) - fetch(`${API_URL}/api/site`) + const controller = new AbortController() + const timeoutId = window.setTimeout(() => controller.abort(), SITE_TIMEOUT_MS) + fetch(`${API_URL}/api/site`, { signal: controller.signal }) .then((res) => { if (!res.ok) throw new Error(`HTTP ${res.status}`) return res.json() as Promise @@ -255,14 +263,17 @@ export function SiteProvider({ children }: { children: ReactNode }) { const res = await fetch(`${API_URL}/api/admin/articles`, { headers: { Authorization: `Bearer ${getToken()}` }, }) - if (res.ok) { + if (res.status === 401) { + // توکن منقضی/نامعتبر — پاک می‌شود تا بارگذاری‌های بعدی درخواست بی‌فایده نفرستند + setToken(null) + } else if (res.ok) { const all = (await res.json()) as Article[] const published = new Set(json.articles.map((a) => a.id)) const drafts = all.filter((a) => a.status === 'draft' && !published.has(a.id)) json.articles = [...json.articles, ...drafts] } } catch { - /* بدون پیش‌نویس‌ها ادامه بده */ + /* بدون پیش‌نویس‌ها ادامه بده — بارگذاری سایت هرگز وابسته به پنل مدیریتی نیست */ } } if (!cancelled) { @@ -275,6 +286,7 @@ export function SiteProvider({ children }: { children: ReactNode }) { }) return () => { cancelled = true + window.clearTimeout(timeoutId) } }, []) diff --git a/frontend/src/components/ArticleCard.tsx b/frontend/src/components/ArticleCard.tsx index a44fea6..efd725e 100644 --- a/frontend/src/components/ArticleCard.tsx +++ b/frontend/src/components/ArticleCard.tsx @@ -26,28 +26,28 @@ export function ArticleCard({ article }: { article: Article }) { -
-

+
+

{article.title}

-

+

{article.excerpt}

-
- +
+ {formatJalali(article.date)} - + {toPersianDigits(article.read)} {/* دکمه کپسولی ادامه مطلب با افکت شیشه‌ای */} - + {t('readMore')} - +
diff --git a/frontend/src/components/Button.tsx b/frontend/src/components/Button.tsx deleted file mode 100644 index b80958d..0000000 --- a/frontend/src/components/Button.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import React from 'react' -import { Link } from 'react-router-dom' - -type Variant = 'copper' | 'outline' | 'dark' | 'dark-ivory' - -type BaseProps = { - children: React.ReactNode - variant?: Variant - className?: string -} - -// استایل‌های فوق‌لوکس شیشه‌ای (Glassmorphic) با رنگ‌های اختصاصی پروژه -const STYLES: Record = { - copper: { - // دکمه اصلی: شیشه دودی تیره با خط نوری مسی، هاله بازتاب و حاشیه کریستالی - base: 'bg-char/75 backdrop-blur-xl text-ivory border border-copper/50 shadow-[inset_0_1px_1px_rgba(255,255,255,0.25),0_8px_20px_rgba(0,0,0,0.4)]', - fill: 'bg-gradient-to-r from-copper via-[#d89f6e] to-copper', - text: 'group-hover:text-char font-semibold', - glow: 'hover:border-copper hover:shadow-[0_0_30px_rgba(197,143,92,0.5)]', - }, - outline: { - // دکمه ثانویه: شیشه دودی کاملاً مات (Frosted Smoked Glass) با بازتاب لبه‌ها - base: 'bg-white/[0.04] backdrop-blur-xl border border-white/20 text-ivory shadow-[inset_0_1px_1px_rgba(255,255,255,0.15),0_8px_20px_rgba(0,0,0,0.3)]', - fill: 'bg-white/10', - text: 'group-hover:text-copper', - glow: 'hover:border-copper/70 hover:bg-white/[0.08] hover:shadow-[0_0_25px_rgba(197,143,92,0.25)]', - }, - dark: { - base: 'bg-ink/85 backdrop-blur-xl text-ivory border border-white/10 shadow-[inset_0_1px_1px_rgba(255,255,255,0.1)]', - fill: 'bg-copper', - text: 'group-hover:text-char', - glow: 'hover:border-copper/50 hover:shadow-[0_0_20px_rgba(197,143,92,0.3)]', - }, - 'dark-ivory': { - base: 'bg-ink/85 backdrop-blur-xl text-ivory border border-white/10 shadow-[inset_0_1px_1px_rgba(255,255,255,0.1)]', - fill: 'bg-ivory', - text: 'group-hover:text-char', - glow: 'hover:shadow-[0_0_20px_rgba(255,255,255,0.2)]', - }, -} - -function Inner({ children, variant = 'copper' }: { children: React.ReactNode; variant?: Variant }) { - const style = STYLES[variant] - return ( - <> - {/* خط انعکاس نوری شیشه در بالای دکمه */} - - - {/* انیمیشن پر شدن موجی و نرم */} -
) : (
@@ -253,21 +251,17 @@ export function ContactSection() {

- +
)} diff --git a/frontend/src/components/LangToggle.tsx b/frontend/src/components/LangToggle.tsx index c85a56d..54906c2 100644 --- a/frontend/src/components/LangToggle.tsx +++ b/frontend/src/components/LangToggle.tsx @@ -1,7 +1,18 @@ import { useLocation, useNavigate } from 'react-router-dom' -import { LanguagesIcon } from 'lucide-react' import { counterpartPath, useLang, writeLangCookie } from '../i18n/lang' +/** + * تنظیمات ثابت هر زبان. + * ترتیب اینجا = ترتیب نمایش در کپسول (فارسی سمت چپ، English سمت راست). + * بجای مقادیر پیکسلی، از translate-x-full استفاده شده تا دقیقاً به اندازه عرض خودش (50%) جابجا شود. + */ +const LANGS = { + fa: { label: 'فارسی', sliderTranslate: 'translate-x-0' }, + en: { label: 'English', sliderTranslate: 'translate-x-full' }, +} as const + +type LangKey = keyof typeof LANGS + /** دکمه تغییر زبان — بین نسخه فارسی و /en انگلیسی همان صفحه جابه‌جا می‌شود */ export function LangToggle({ className = '' }: { className?: string }) { const { lang, t } = useLang() @@ -22,10 +33,61 @@ export function LangToggle({ className = '' }: { className?: string }) { onClick={switchLang} aria-label={t('switchLangAria')} title={t('switchLangAria')} - className={`group flex items-center gap-1.5 rounded-full border border-white/15 bg-white/5 px-3.5 py-2 text-xs font-bold text-ivory/80 backdrop-blur-lg shadow-[inset_0_1px_1px_rgba(255,255,255,0.15)] transition-all duration-300 ease-lux hover:border-copper/60 hover:bg-white/10 hover:text-copper active:scale-95 cursor-pointer ${className}`} + dir="ltr" + className={[ + // ── Capsule shell ────────────────────────────────────────────── + 'relative flex items-center', + 'w-[152px] h-[42px] rounded-full p-1', // استفاده از p-1 (4px) به جای [4px] + 'bg-[#1a1715]', + // deep inset shadow → recessed track feel + 'shadow-[inset_0_3px_7px_rgba(0,0,0,0.75),inset_0_-1px_2px_rgba(255,255,255,0.04)]', + // interaction + 'cursor-pointer select-none', + 'active:scale-[0.975] transition-transform duration-150 ease-lux', + className, + ].join(' ')} > - - {t('switchLang')} + {/* ── Inner Track ─────────────────────────────────────────────── + این دیو داخلی باعث می‌شود پدینگ دکمه اصلی روی محاسبات 50 درصدی تاثیر نگذارد + */} +
+ + {/* ── Slider pill ─────────────────────────────────────────────── */} +
) -} +} \ No newline at end of file diff --git a/frontend/src/components/Lightbox.tsx b/frontend/src/components/Lightbox.tsx index d78abf9..a1b732e 100644 --- a/frontend/src/components/Lightbox.tsx +++ b/frontend/src/components/Lightbox.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect } from 'react' import { AnimatePresence, motion } from 'framer-motion' -import { XIcon, ChevronLeftIcon, ChevronRightIcon } from 'lucide-react' +import { MdClose, MdChevronLeft, MdChevronRight } from 'react-icons/md' type Props = { images: string[] @@ -26,8 +26,8 @@ export function Lightbox({ images, index, onClose, onChange }: Props) { if (!isOpen) return const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose() - if (e.key === 'ArrowRight') prev() - if (e.key === 'ArrowLeft') next() + if (e.key === 'ArrowRight') next() + if (e.key === 'ArrowLeft') prev() } window.addEventListener('keydown', onKey) document.body.style.overflow = 'hidden' @@ -43,55 +43,67 @@ export function Lightbox({ images, index, onClose, onChange }: Props) { + {/* Solid Brand Background */}