Add a completely separate urban redesign system allowing users to create proposals with pre-made building/infrastructure shapes placed on a map. Roads will be path-based with configurable widths, while buildings/parks use fixed footprints.
Gemfile- Add devise gemconfig/initializers/devise.rb- Devise configurationapp/models/user.rb- User modeldb/migrate/*_devise_create_users.rb- User table migrationapp/controllers/application_controller.rb- Update auth methodapp/views/devise/**/*- Login/registration views
- Add
devisegem to Gemfile - Run
rails generate devise:install - Generate User model:
rails generate devise User - Add additional fields to users migration:
username:string:uniq,name:string - Run migration
- Generate Devise views:
rails generate devise:views - Update routes to include Devise routes
- Replace
authenticate_adminwith Devise'sauthenticate_user!where needed - Keep existing HTTP Basic Auth for backward compatibility (optional)
- Add login/logout links to navigation
Notes:
- Existing incidents functionality should remain mostly public (only create/edit/delete protected)
- New redesign features will require user authentication
- Each redesign proposal will be associated with a user
config/shape_templates.yml- YAML config for predefined shapesapp/models/shape_template.rb- Database model for custom templatesdb/migrate/*_create_shape_templates.rb- Shape templates tablelib/shape_template_loader.rb- Loads YAML templates into memory
residential:
single_family:
name: "Single Family Home"
category: "residential"
width: 15
depth: 12
color: "#FFD700"
townhome:
name: "Townhome"
category: "residential"
width: 8
depth: 20
color: "#FFA500"
apartment_4story:
name: "4-Story Apartment"
category: "residential"
width: 30
depth: 25
color: "#FF6347"
commercial:
retail_small:
name: "Small Retail"
category: "commercial"
width: 20
depth: 15
color: "#4169E1"
mixed_use:
name: "Mixed Use Building"
category: "commercial"
width: 25
depth: 30
color: "#6A5ACD"
parks:
pocket_park:
name: "Pocket Park"
category: "parks"
width: 20
depth: 20
color: "#228B22"
plaza:
name: "Plaza"
category: "parks"
width: 40
depth: 40
color: "#32CD32"
infrastructure:
bike_lane:
name: "Bike Lane"
category: "infrastructure"
width: 2
is_path: true
color: "#00CED1"id- Primary keyname- String, not nullcategory- String, not null (residential, commercial, parks, infrastructure, roads)width- Float (meters)depth- Float (meters, null for paths)color- String (hex color)is_path- Boolean (default false)user_id- Foreign key (null for system templates)template_data- JSONB (for custom properties)created_at,updated_at
app/models/redesign.rb- Main proposal modelapp/models/placed_shape.rb- Individual shapes within a proposalapp/models/road_node.rb- Nodes for road pathsdb/migrate/*_create_redesigns.rbdb/migrate/*_create_placed_shapes.rbdb/migrate/*_create_road_nodes.rb
id- Primary keyuser_id- Foreign key to users, not nullname- String, not nulldescription- Textcenter_latitude- Float, not nullcenter_longitude- Float, not nullmax_radius- Float (meters), not null, default 500created_at,updated_at
id- Primary keyredesign_id- Foreign key, not nullshape_template_id- Foreign key (nullable for roads)shape_type- String (building, park, road)latitude- Float, not nulllongitude- Float, not nullrotation- Float (degrees, 0-360), default 0width- Float (override template width)depth- Float (override template depth)color- String (override template color)metadata- JSONB (for custom properties)created_at,updated_at
id- Primary keyplaced_shape_id- Foreign key, not null (references parent road)latitude- Float, not nulllongitude- Float, not nullsequence_order- Integer, not nullcreated_at,updated_at- Index on
[placed_shape_id, sequence_order]
Redesign:
belongs_to :userhas_many :placed_shapes, dependent: :destroy- Validates presence of name, center_latitude, center_longitude, max_radius
- Validates max_radius > 0
PlacedShape:
belongs_to :redesignbelongs_to :shape_template, optional: truehas_many :road_nodes, -> { order(:sequence_order) }, dependent: :destroy- Validates presence of redesign_id, latitude, longitude
- Validates rotation between 0-360
- Custom validation: roads must have road_nodes, buildings must have shape_template
RoadNode:
belongs_to :placed_shape- Validates presence of latitude, longitude, sequence_order
app/controllers/api/v1/redesigns_controller.rb- CRUD for proposalsapp/controllers/api/v1/placed_shapes_controller.rb- CRUD for shapesapp/controllers/api/v1/shape_templates_controller.rb- Template listingconfig/routes.rb- Add API routes
namespace :api do
namespace :v1 do
resources :redesigns do
resources :placed_shapes, only: [:create, :update, :destroy]
member do
get :geojson
end
end
resources :shape_templates, only: [:index]
end
endGET /api/v1/shape_templates
- Returns combined list of YAML templates + custom user templates
- Response:
{ templates: [{ id, name, category, width, depth, color, is_path, is_custom }] }
POST /api/v1/redesigns
- Creates new redesign proposal
- Params:
{ name, description, center_latitude, center_longitude, max_radius } - Requires authentication
- Response:
{ id, name, description, center_latitude, center_longitude, max_radius, created_at }
GET /api/v1/redesigns/:id
- Returns single redesign with all placed shapes and road nodes
- Response includes full GeoJSON representation
GET /api/v1/redesigns/:id/geojson
- Returns redesign as GeoJSON FeatureCollection
- Each placed shape is a Feature with geometry based on type:
- Buildings/parks: Polygon (calculated from center + width/depth + rotation)
- Roads: LineString (from ordered road_nodes)
- Properties include shape_template info, color, metadata
POST /api/v1/redesigns/:redesign_id/placed_shapes
- Creates new shape within redesign
- Params:
{ shape_template_id, latitude, longitude, rotation, road_nodes: [{lat, lng, sequence}] } - Response: Created shape with ID
PATCH /api/v1/redesigns/:redesign_id/placed_shapes/:id
- Updates shape position, rotation, or road nodes
- Params:
{ latitude, longitude, rotation, road_nodes: [{id, lat, lng, sequence}] }
DELETE /api/v1/redesigns/:redesign_id/placed_shapes/:id
- Soft delete or hard delete shape (cascade deletes road_nodes)
app/controllers/redesigns_controller.rb- Web controllerapp/views/redesigns/index.html.erb- List user's redesignsapp/views/redesigns/show.html.erb- Main editing interfaceapp/views/redesigns/new.html.erb- Create new redesign formconfig/routes.rb- Add web routes
resources :redesigns do
member do
get :edit_map
end
endindex- List current user's redesignsnew- Form to create new redesigncreate- Handle form submissionshow- Main editing interface with map + sidebaredit- Edit redesign metadataupdate- Update metadatadestroy- Delete redesign
app/javascript/controllers/redesign_map_controller.ts- Main map controllerapp/javascript/controllers/shape_palette_controller.ts- Shape selection sidebarapp/javascript/controllers/shape_placer_controller.ts- Shape placement logicapp/javascript/controllers/road_drawer_controller.ts- Road drawing tool
Responsibilities:
- Initialize Mapbox map centered on redesign center point
- Render all placed shapes as GeoJSON layers
- Handle map clicks for shape placement
- Manage selection state for editing shapes
- Sync with backend API for persistence
Targets:
container- Map container div
Values:
redesignId- Current redesign IDcenterLat- Center latitudecenterLng- Center longitudemaxRadius- Maximum radius for boundary circle
Actions:
connect()- Initialize map, load GeoJSON, draw boundary circleloadShapes()- Fetch from/api/v1/redesigns/:id/geojsonaddShapeToMap(shape)- Add GeoJSON feature to mapremoveShapeFromMap(shapeId)- Remove feature from mapselectShape(shapeId)- Highlight selected shapeupdateShape(shapeId, updates)- Update shape position/rotationhandleMapClick(e)- Delegate to active tool (shape placer or road drawer)
Events Listened:
shape-palette:shapeSelected- Activate shape placement modeshape-placer:shapePlaced- Add new shape to maproad-drawer:roadCompleted- Add new road to map
Events Dispatched:
shape-selected- When user clicks existing shapeshape-updated- After successful API update
Responsibilities:
- Display categorized list of shape templates
- Handle shape selection
- Show selected shape details
- Trigger shape placement mode
Targets:
category- Category sections (residential, commercial, etc.)shapeButton- Individual shape buttonsselectedInfo- Selected shape info panel
Values:
selectedTemplateId- Currently selected template ID
Actions:
connect()- Load templates from/api/v1/shape_templatesselectShape(e)- Handle shape button clickrenderTemplates(templates)- Build sidebar UIclearSelection()- Deselect current shape
Events Dispatched:
shapeSelected- When user clicks shape, includes template data
Responsibilities:
- Handle click-to-place for buildings/parks
- Show preview of shape at cursor position
- Calculate polygon geometry from center point + dimensions + rotation
- Create shape via API
Values:
activeTemplate- Current template being placedpreviewVisible- Boolean for preview state
Actions:
connect()- Listen for shape selection eventsactivatePlacement(template)- Enter placement modeupdatePreview(lat, lng)- Show shape preview at cursorplaceShape(lat, lng)- POST to API, dispatch completion eventcalculatePolygon(lat, lng, width, depth, rotation)- Generate polygon coordinatescancel()- Exit placement mode
Events Listened:
shape-palette:shapeSelected- Activate placement with template
Events Dispatched:
shapePlaced- After successful API creation
Responsibilities:
- Multi-click road path drawing
- Manage road nodes array
- Show line preview while drawing
- Handle curve smoothing (future enhancement)
- Lane count selector (popover or toolbar)
- Create road via API
Values:
nodes- Array of {lat, lng} objectsisDrawing- BooleanlaneCount- Number (2-6)roadWidth- Calculated from lane count
Actions:
connect()- Initialize drawing statestartDrawing()- Enter road drawing modeaddNode(lat, lng)- Add node to pathupdatePreview()- Show line previewcompleteRoad()- POST to API with nodes arrayundoLastNode()- Remove last node (while drawing)cancel()- Exit drawing modesetLaneCount(count)- Update lane count (recalculates width)
Events Listened:
road-tool:activated- Start drawing mode
Events Dispatched:
roadCompleted- After successful API creation
app/views/redesigns/show.html.erb- Main editing interfaceapp/views/redesigns/_shape_palette.html.erb- Sidebar partialapp/assets/stylesheets/redesigns.scss- Styling
<div class="redesign-editor" data-controller="redesign-map">
<div class="redesign-sidebar">
<%= render 'shape_palette' %>
</div>
<div class="redesign-map-container"
data-redesign-map-target="container"
data-redesign-map-redesign-id-value="<%= @redesign.id %>"
data-redesign-map-center-lat-value="<%= @redesign.center_latitude %>"
data-redesign-map-center-lng-value="<%= @redesign.center_longitude %>">
</div>
<div class="redesign-toolbar">
<button data-action="road-drawer#activate">Draw Road</button>
<button data-action="redesign-map#undo">Undo</button>
<button data-action="redesign-map#save">Save</button>
</div>
</div>Key Differences from Incidents Dashboard:
- Fixed sidebar (no overlay, no draggable divider)
- Sidebar is thin column (20-25% width, maybe 300-400px fixed)
- Map takes remaining width
- Toolbar at bottom or top for road drawing controls
<div class="shape-palette" data-controller="shape-palette">
<h3>Shape Templates</h3>
<div class="palette-section" data-shape-palette-target="category">
<h4>Residential</h4>
<div class="shape-grid">
<!-- Shape buttons rendered dynamically -->
</div>
</div>
<div class="palette-section" data-shape-palette-target="category">
<h4>Commercial</h4>
<div class="shape-grid">
<!-- Shape buttons rendered dynamically -->
</div>
</div>
<!-- More categories... -->
<div class="selected-info" data-shape-palette-target="selectedInfo">
<!-- Selected shape details -->
</div>
</div>- Use existing TailwindCSS setup
- Color-coded shape buttons matching template colors
- Hover previews showing dimensions
- Active state for selected template
- Map container fills remaining space
- Toolbar buttons styled consistently with existing app
app/javascript/controllers/redesign_map_controller.ts- Add rendering logic
Buildings/Parks (Polygon Features):
- Calculate 4 corner points from center + width/depth + rotation
- Convert to GeoJSON Polygon geometry
- Add to Mapbox as fill layer with stroke
- Style with template color
- Enable click for selection/editing
Roads (LineString Features):
- Convert ordered road_nodes to coordinates array
- Create GeoJSON LineString geometry
- Add to Mapbox as line layer
- Width based on lane count (3.5m per lane)
- Style with road color (gray/black)
- Show lane markings (future enhancement)
Boundary Circle:
- Use Mapbox circle layer or turf.js to draw max_radius circle
- Dashed stroke, semi-transparent fill
- Indicates workspace boundary
- Base map style
- Boundary circle
- Road lines (thicker, below buildings)
- Building/park fills
- Building/park strokes
- Selected shape highlight
- Preview layer (during placement)
- Default: Shapes are clickable for selection
- Placement Mode: Crosshair cursor, click to place
- Road Drawing: Crosshair, clicks add nodes
- Selected: Shape highlighted, shows drag handles (future)
- Phase 1: User authentication (Devise) - Foundation for everything
- Phase 2: Shape template system - Define available shapes
- Phase 3: Core models - Data structure for redesigns
- Phase 4: API endpoints - Backend CRUD operations
- Phase 5: Web routes/controllers - Basic web interface
- Phase 6: Stimulus controllers - Interactive map functionality
- Phase 7: Views and styling - UI polish
- Phase 8: GeoJSON rendering - Visual representation
-
Road width calculation: Use standard 3.5m per lane. Simple lane count selector (2-6 lanes). Can add road types later.
-
Shape rotation: Free rotation 0-360° with 15° snap points (hold shift to disable snap). Allows creativity while making grid alignment easy.
-
Road joining logic: Start with visual overlap only. No auto-snapping or intersection nodes in MVP. This keeps the data model simple and lets users manually align roads.
-
Placement interaction: Start with click-to-select + click-to-place (simpler). Drag-and-drop from sidebar can be added as enhancement later.
-
Multiple proposals: Allow unlimited proposals per user, even for overlapping areas. Users might want to compare different redesign approaches for the same location.
-
Visibility: Start with private-only (creator can view/edit their own). Add public sharing/publishing feature in a later phase.
- Roads are the most complex feature due to path-based geometry
- Consider starting with simple buildings/parks to validate the placement system
- Road drawing could be a separate phase/enhancement
- Mapbox Draw plugin might be useful for road path editing
- Consider using turf.js for geometry calculations (polygon generation, distance, etc.)