Spaces:
Running
Running
| import dash_bootstrap_components as dbc | |
| import dash_leaflet as dl | |
| from dash import dcc, html | |
| from config import ( | |
| BASEMAP_URL, BASEMAP_ATTRIBUTION, | |
| MAP_CENTER, MAP_ZOOM, | |
| COLOR_POLYGON, COLOR_MMH, | |
| ANALYSIS_LAYERS, CONTEXT_LAYERS, | |
| SIDEBAR_BG, CARD_BG, TEXT_PRIMARY, TEXT_MUTED, | |
| ) | |
| from queries import get_geography_options | |
| def make_layout() -> html.Div: | |
| geo_options = get_geography_options() | |
| map_children = [ | |
| dl.TileLayer( | |
| url=BASEMAP_URL, | |
| attribution=BASEMAP_ATTRIBUTION, | |
| maxZoom=20, | |
| ), | |
| dl.GeoJSON(id="ctx-villages", data=None, | |
| options={"style": {"color": "#7B8CDE", "weight": 1.5, | |
| "fillColor": "#7B8CDE", "fillOpacity": 0.03}}), | |
| dl.GeoJSON(id="ctx-zoning", data=None, | |
| options={"style": {"color": "#FF6B6B", "weight": 0.8, | |
| "fillOpacity": 0.0}}), | |
| dl.GeoJSON(id="ctx-citylimits", data=None, | |
| options={"style": {"color": "#FFFFFF", "weight": 2, | |
| "fillOpacity": 0.0, "dashArray": "8 4"}}), | |
| dl.GeoJSON(id="ctx-dpi", data=None, | |
| options={"style": {"color": "#69F0AE", "weight": 2, | |
| "fillOpacity": 0.0, "dashArray": "4 4"}}), | |
| dl.GeoJSON(id="ctx-lightrail", data=None, | |
| options={"style": {"color": "#FFD700", "weight": 3, | |
| "fillOpacity": 0.0}}), | |
| dl.GeoJSON(id="ctx-arterials", data=None, | |
| options={"style": {"color": "#FF9800", "weight": 1.5, | |
| "fillOpacity": 0.0}}), | |
| dl.GeoJSON(id="ctx-parks", data=None, | |
| options={"style": {"color": "#4CAF50", "weight": 1, | |
| "fillColor": "#4CAF50", "fillOpacity": 0.2}}), | |
| dl.GeoJSON( | |
| id="choropleth-layer", | |
| data=None, | |
| style=dict(weight=0.8, fillOpacity=0.65, | |
| color=COLOR_MMH, fillColor=COLOR_MMH), | |
| clickData=True, | |
| zoomToBoundsOnClick=False, | |
| ), | |
| dl.GeoJSON( | |
| id="active-polygon-layer", | |
| data=None, | |
| options={"style": { | |
| "color": COLOR_POLYGON, | |
| "weight": 2.5, | |
| "fillColor": COLOR_POLYGON, | |
| "fillOpacity": 0.08, | |
| "dashArray": "6 4", | |
| }}, | |
| ), | |
| dl.FeatureGroup(children=[ | |
| dl.EditControl( | |
| id="edit-control", | |
| position="topleft", | |
| draw={ | |
| "polygon": {"allowIntersection": False, | |
| "shapeOptions": {"color": COLOR_POLYGON, | |
| "weight": 2}}, | |
| "rectangle": {"shapeOptions": {"color": COLOR_POLYGON, | |
| "weight": 2}}, | |
| "circle": False, | |
| "circlemarker": False, | |
| "marker": False, | |
| "polyline": False, | |
| }, | |
| edit={"edit": True, "remove": True}, | |
| ), | |
| ]), | |
| ] | |
| map_component = dl.Map( | |
| id="main-map", | |
| center=MAP_CENTER, | |
| zoom=MAP_ZOOM, | |
| style={"height": "100vh", "width": "100%"}, | |
| children=map_children, | |
| ) | |
| layer_panel = html.Div( | |
| style={ | |
| "position": "absolute", "top": "10px", "right": "10px", | |
| "zIndex": 1000, "width": "220px", | |
| }, | |
| children=[ | |
| dbc.Button( | |
| "⊞ Layers", | |
| id="layer-panel-toggle", | |
| size="sm", | |
| style={ | |
| "background": "rgba(22,33,62,0.95)", | |
| "border": "1px solid #2A2A4A", | |
| "color": TEXT_PRIMARY, | |
| "width": "100%", | |
| "textAlign": "left", | |
| "marginBottom": "2px", | |
| "fontSize": "0.8rem", | |
| }, | |
| n_clicks=0, | |
| ), | |
| dbc.Collapse( | |
| id="layer-panel-collapse", | |
| is_open=True, | |
| children=html.Div( | |
| style={ | |
| "background": "rgba(22,33,62,0.95)", | |
| "border": "1px solid #2A2A4A", | |
| "borderRadius": "4px", | |
| "padding": "10px 12px", | |
| }, | |
| children=[ | |
| html.P("ANALYSIS LAYER", | |
| style={"color": "#7B8CDE", "fontSize": "0.62rem", | |
| "fontWeight": 700, "letterSpacing": "0.1em", | |
| "margin": "0 0 6px"}), | |
| dcc.RadioItems( | |
| id="analysis-layer-radio", | |
| options=[ | |
| { | |
| "label": html.Span([ | |
| html.Span("● ", style={"color": l["color"], | |
| "fontSize": "1.1em"}), | |
| l["label"], | |
| ], style={"fontSize": "0.8rem", | |
| "color": TEXT_PRIMARY}), | |
| "value": l["value"], | |
| } | |
| for l in ANALYSIS_LAYERS | |
| ], | |
| value="mmh", | |
| inputStyle={"marginRight": "6px"}, | |
| labelStyle={"display": "flex", "alignItems": "center", | |
| "marginBottom": "4px"}, | |
| ), | |
| html.Hr(style={"borderColor": "#2A2A4A", "margin": "8px 0"}), | |
| html.P("CONTEXT LAYERS", | |
| style={"color": "#7B8CDE", "fontSize": "0.62rem", | |
| "fontWeight": 700, "letterSpacing": "0.1em", | |
| "margin": "0 0 6px"}), | |
| dcc.Checklist( | |
| id="context-layers-checklist", | |
| options=[ | |
| { | |
| "label": html.Span([ | |
| html.Span("— ", style={"color": l["color"], | |
| "fontWeight": 700}), | |
| l["label"], | |
| ], style={"fontSize": "0.8rem", | |
| "color": TEXT_PRIMARY}), | |
| "value": l["value"], | |
| } | |
| for l in CONTEXT_LAYERS | |
| ], | |
| value=["villages"], | |
| inputStyle={"marginRight": "6px"}, | |
| labelStyle={"display": "flex", "alignItems": "center", | |
| "marginBottom": "4px"}, | |
| ), | |
| ], | |
| ), | |
| ), | |
| ], | |
| ) | |
| sidebar = html.Div( | |
| id="sidebar", | |
| style={ | |
| "background": SIDEBAR_BG, | |
| "height": "100vh", | |
| "overflowY": "auto", | |
| "padding": "16px", | |
| }, | |
| children=[ | |
| html.Div([ | |
| html.H5("Phoenix Upzoning Scanner", | |
| style={"color": TEXT_PRIMARY, "margin": 0, | |
| "fontWeight": 700}), | |
| html.P("Parcel-level MMH feasibility analysis", | |
| style={"color": TEXT_MUTED, "fontSize": "0.8rem", | |
| "margin": 0}), | |
| ], className="mb-3"), | |
| html.Hr(style={"borderColor": "#333", "margin": "8px 0 16px"}), | |
| html.P("ANALYZE AREA", className="section-label"), | |
| dcc.Dropdown( | |
| id="geography-dropdown", | |
| options=geo_options, | |
| value="", | |
| placeholder="Select Urban Village or TOD district…", | |
| clearable=True, | |
| className="dark-dropdown mb-2", | |
| ), | |
| html.P( | |
| "— or draw a polygon on the map —", | |
| style={"color": TEXT_MUTED, "fontSize": "0.75rem", | |
| "textAlign": "center", "margin": "4px 0 12px"}, | |
| ), | |
| dbc.Button( | |
| "✕ Clear polygon", | |
| id="clear-btn", | |
| color="secondary", | |
| outline=True, | |
| size="sm", | |
| className="w-100 mb-3", | |
| style={"fontSize": "0.75rem"}, | |
| ), | |
| html.Hr(style={"borderColor": "#333", "margin": "8px 0 16px"}), | |
| html.P("FILTER BY ZONE", className="section-label"), | |
| dcc.Dropdown( | |
| id="zone-filter-dropdown", | |
| options=[], | |
| value=None, | |
| placeholder="All zones…", | |
| clearable=True, | |
| multi=True, | |
| className="dark-dropdown mb-3", | |
| ), | |
| html.Hr(style={"borderColor": "#333", "margin": "8px 0 16px"}), | |
| html.Div(id="scorecard-area"), | |
| ], | |
| ) | |
| parcel_modal = dbc.Modal([ | |
| dbc.ModalHeader(dbc.ModalTitle(id="parcel-modal-title")), | |
| dbc.ModalBody(id="parcel-modal-body"), | |
| dbc.ModalFooter( | |
| dbc.Button("Close", id="parcel-modal-close", | |
| className="ms-auto", n_clicks=0) | |
| ), | |
| ], id="parcel-modal", size="lg", is_open=False, scrollable=True) | |
| stores = html.Div([ | |
| dcc.Store(id="active-polygon-store"), | |
| dcc.Store(id="active-layer-store", data="mmh"), | |
| dcc.Store(id="zone-filter-store", data=None), | |
| ]) | |
| return html.Div([ | |
| stores, | |
| parcel_modal, | |
| html.Div( | |
| style={"position": "relative", "height": "100vh"}, | |
| children=[ | |
| dbc.Row( | |
| [ | |
| dbc.Col(map_component, width=8, className="p-0"), | |
| dbc.Col(sidebar, width=4, className="p-0"), | |
| ], | |
| className="g-0", | |
| style={"height": "100vh", "overflow": "hidden"}, | |
| ), | |
| layer_panel, | |
| ], | |
| ), | |
| ]) |