wip stairs impl

This commit is contained in:
Degra02 2025-08-02 01:12:56 +02:00
parent 603329fbda
commit 2c840e5e20
9 changed files with 941 additions and 43 deletions

View file

@ -42,6 +42,7 @@ def geometrystyles(request):
'door': '#ffffff',
'area': '#55aaff',
'stair': '#a000a0',
'stairway': '#b000b0',
'ramp': 'rgba(160, 0, 160, 0.2)',
'obstacle': '#999999',
'lineobstacle': '#999999',

View file

@ -402,7 +402,7 @@ def create_editor_form(editor_model):
'stroke_opacity', 'fill_color', 'fill_opacity', 'interactive', 'point_icon', 'extra_data', 'show_label',
'show_geometry', 'show_label', 'show_geometry', 'default_geomtype', 'cluster_points', 'update_interval',
'load_group_display', 'load_group_contribute',
'altitude_quest', 'fill_quest',
'altitude_quest', 'fill_quest', 'stair_count', 'stair_width',
]
field_names = [field.name for field in editor_model._meta.get_fields()
if not field.one_to_many and not isinstance(field, ManyToManyRel)]

View file

@ -619,4 +619,158 @@ label.theme-color-label {
opacity: 0.8;
transform: scale(1);
}
}
/* Stairway Creator control styles */
.leaflet-control-stairway {
background-color: white;
border-radius: 4px;
border: 2px solid rgba(0,0,0,0.2);
background-clip: padding-box;
margin-top: 45px; // Position below snap control
.stairway-toggle {
display: block;
width: 30px;
height: 30px;
line-height: 26px;
text-align: center;
text-decoration: none;
font-size: 18px;
background-color: white;
color: #666;
border-radius: 2px;
transition: all 0.2s ease;
&:hover {
background-color: #f4f4f4;
color: #333;
}
&.active {
background-color: #FF9800;
color: white;
&:hover {
background-color: #F57C00;
}
}
}
}
/* Stairway Creator configuration panel */
#stairway-config {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 5px;
padding: 15px;
margin-bottom: 15px;
h4 {
margin-top: 0;
margin-bottom: 15px;
color: #FF9800;
font-weight: bold;
}
.form-group {
margin-bottom: 12px;
label {
font-weight: 600;
color: #495057;
margin-bottom: 5px;
}
.form-control {
border: 1px solid #ced4da;
border-radius: 3px;
&:focus {
border-color: #FF9800;
box-shadow: 0 0 0 0.2rem rgba(255, 152, 0, 0.25);
}
}
}
.btn-group {
margin-top: 15px;
.btn {
margin-right: 10px;
}
.btn-primary {
background-color: #FF9800;
border-color: #FF9800;
&:hover {
background-color: #F57C00;
border-color: #F57C00;
}
&:disabled {
background-color: #6c757d;
border-color: #6c757d;
}
}
}
}
/* Stairway Creator instructions */
#stairway-instructions {
border-left: 4px solid #FF9800;
background-color: #fff3cd;
border-color: #ffeaa7;
color: #856404;
margin-bottom: 15px;
strong {
color: #FF9800;
}
}
/* Stairway preview elements */
.stairway-point-label {
background-color: #ff6b6b;
color: white;
border-radius: 50%;
text-align: center;
font-weight: bold;
font-size: 12px;
line-height: 20px;
border: 2px solid white;
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
.stairway-step-label {
background-color: #4CAF50;
color: white;
border-radius: 50%;
text-align: center;
font-weight: bold;
font-size: 10px;
line-height: 16px;
border: 1px solid white;
box-shadow: 0 1px 2px rgba(0,0,0,0.3);
}
/* Stairway step preview lines */
.stairway-step-preview {
pointer-events: none;
z-index: 500;
}
/* Alert styles for stairway creator */
.alert {
padding: 12px;
margin-bottom: 15px;
border: 1px solid transparent;
border-radius: 4px;
&.alert-info {
color: #0c5460;
background-color: #d1ecf1;
border-color: #bee5eb;
}
}

View file

@ -182,9 +182,12 @@ editor = {
$('#sidebar').addClass('loading').find('.content').html('');
editor._cancel_editing();
// Clear snap indicators when unloading
editor._clear_snap_indicators();
// Clear stairway creator state when unloading
editor._clear_stairway_creator_state();
},
_fill_level_control: function (level_control, level_list, geometryURLs) {
var levels = level_list.find('a');
@ -731,6 +734,7 @@ editor = {
_orig_vertex_pos: null,
_max_bounds: null,
_creating_type: null,
_creating_model_type: null,
_shift_pressed: false,
init_geometries: function () {
// init geometries and edit listeners
@ -821,6 +825,9 @@ editor = {
// Initialize snap-to-edges functionality
editor.init_snap_to_edges();
// Initialize stairway creator functionality
editor.init_stairway_creator();
c3nav_api.get('editor/geometrystyles')
.then(geometrystyles => {
editor.geometrystyles = geometrystyles;
@ -1395,6 +1402,16 @@ editor = {
const startGeomEditing = (geomtype) => {
editor._creating_type = geomtype;
editor._creating = true;
// Store the current model type for business logic
editor._creating_model_type = form.attr('data-new') || null;
// Auto-activate stairway creator for Stairway type
if (editor._creating_model_type === 'stairway') {
setTimeout(() => {
editor._activate_stairway_creator();
}, 100); // Small delay to ensure geometry creation is fully initialized
}
if (editor._current_editing_shape) {
editor._current_editing_shape.remove();
}
@ -1440,6 +1457,7 @@ editor = {
// called on sidebar unload. cancel all editing and creating.
if (editor._creating) {
editor._creating = false;
editor._creating_model_type = null;
editor.map.editTools.stopDrawing();
}
editor._graph_editing = false;
@ -1448,9 +1466,12 @@ editor = {
editor._editing_layer.disableEdit();
editor._editing_layer = null;
}
// Clear snap indicators when canceling editing
editor._clear_snap_indicators();
// Clear stairway creator state when canceling editing
editor._clear_stairway_creator_state();
},
_canceled_creating: function (e) {
// called after we canceled creating so we can remove the temporary layer.
@ -1464,6 +1485,7 @@ editor = {
if (editor._creating_type !== 'multipoint') {
// multipoints can always accept more points so they are always in "creating" mode
editor._creating = false;
editor._creating_model_type = null;
}
var layer = e.layer;
if (editor._creating_type === 'point' && layer._latlng !== undefined) {
@ -1666,18 +1688,18 @@ editor = {
// Snap-to-edges functionality
_snap_enabled: true,
_snap_distance: 15, // pixels
_snap_to_base_map: false,
_snap_to_base_map: false,
_snap_indicator: null,
_snap_candidates: [],
init_snap_to_edges: function() {
// Initialize snap indicator layer
editor._snap_indicator = L.layerGroup().addTo(editor.map);
// Override existing drawing event handlers to include snapping
editor.map.on('editable:drawing:move', editor._handle_snap_during_draw);
editor.map.on('editable:vertex:drag', editor._handle_snap_during_vertex_drag);
// Add snap toggle to UI
editor._add_snap_controls();
},
@ -1687,14 +1709,14 @@ editor = {
var snapControl = L.control({position: 'topleft'});
snapControl.onAdd = function() {
var container = L.DomUtil.create('div', 'leaflet-bar leaflet-control leaflet-control-snap');
container.innerHTML = '<a href="#" title="Toggle Snap to Edges" class="snap-toggle ' +
container.innerHTML = '<a href="#" title="Toggle Snap to Edges" class="snap-toggle ' +
(editor._snap_enabled ? 'active' : '') + '">⚡</a>';
L.DomEvent.on(container.querySelector('.snap-toggle'), 'click', function(e) {
e.preventDefault();
editor._toggle_snap();
});
L.DomEvent.disableClickPropagation(container);
return container;
};
@ -1713,13 +1735,13 @@ editor = {
_handle_snap_during_draw: function(e) {
if (!editor._snap_enabled || !editor._creating) return;
var snapped = editor._find_and_apply_snap(e.latlng);
if (snapped) {
e.latlng.lat = snapped.lat;
e.latlng.lng = snapped.lng;
}
// Apply existing rounding
e.latlng.lat = Math.round(e.latlng.lat * 100) / 100;
e.latlng.lng = Math.round(e.latlng.lng * 100) / 100;
@ -1727,41 +1749,41 @@ editor = {
_handle_snap_during_vertex_drag: function(e) {
if (!editor._snap_enabled) return;
var snapped = editor._find_and_apply_snap(e.latlng);
var snapped = editor._find_and_apply_snap(e.latlng, e.vertex);
if (snapped) {
e.latlng.lat = snapped.lat;
e.latlng.lng = snapped.lng;
}
// Apply existing rounding and other constraints
e.vertex.setLatLng([Math.round(e.latlng.lat * 100) / 100, Math.round(e.latlng.lng * 100) / 100]);
},
_find_and_apply_snap: function(latlng) {
_find_and_apply_snap: function(latlng, currentVertex) {
if (!editor._geometries_layer) return null;
var mapPoint = editor.map.latLngToContainerPoint(latlng);
var candidates = [];
// Find snap candidates from existing geometries
editor._geometries_layer.eachLayer(function(layer) {
if (layer === editor._editing_layer) return; // Don't snap to self
var snapPoint = editor._find_closest_point_on_geometry(layer, latlng, mapPoint);
// if (layer === editor._editing_layer) return; // Don't snap to self
var snapPoint = editor._find_closest_point_on_geometry(layer, latlng, mapPoint, currentVertex);
if (snapPoint && snapPoint.distance < editor._snap_distance) {
candidates.push(snapPoint);
}
});
// Find the closest candidate
if (candidates.length > 0) {
candidates.sort(function(a, b) { return a.distance - b.distance; });
var best = candidates[0];
// Show snap indicator
editor._show_snap_indicator(best.latlng);
// editor._show_snap_indicator(best.latlng);
return best.latlng;
} else {
editor._clear_snap_indicators();
@ -1771,13 +1793,13 @@ editor = {
_find_closest_point_on_geometry: function(layer, targetLatLng, targetMapPoint) {
if (!layer.getLatLngs) return null;
var closestPoint = null;
var closestDistance = Infinity;
try {
var coordinates = [];
// Handle different geometry types
if (layer instanceof L.Polygon || layer instanceof L.Polyline) {
coordinates = layer.getLatLngs();
@ -1797,36 +1819,36 @@ editor = {
}
return null;
}
// Check each edge of the geometry
for (var i = 0; i < coordinates.length; i++) {
var p1 = coordinates[i];
var p2 = coordinates[(i + 1) % coordinates.length];
var snapPoint = editor._find_closest_point_on_edge(p1, p2, targetLatLng, targetMapPoint);
if (snapPoint && snapPoint.distance < closestDistance) {
closestDistance = snapPoint.distance;
closestPoint = snapPoint;
}
}
}
} catch (error) {
// Silently handle geometry access errors
return null;
}
return closestPoint;
},
_find_closest_point_on_edge: function(p1, p2, targetLatLng, targetMapPoint) {
var p1Map = editor.map.latLngToContainerPoint(p1);
var p2Map = editor.map.latLngToContainerPoint(p2);
// Find closest point on line segment
var dx = p2Map.x - p1Map.x;
var dy = p2Map.y - p1Map.y;
var length = Math.sqrt(dx * dx + dy * dy);
if (length === 0) {
// Points are the same, snap to the point
var distance = p1Map.distanceTo(targetMapPoint);
@ -1835,25 +1857,25 @@ editor = {
distance: distance
};
}
// Calculate parameter t for closest point on line
var t = ((targetMapPoint.x - p1Map.x) * dx + (targetMapPoint.y - p1Map.y) * dy) / (length * length);
t = Math.max(0, Math.min(1, t)); // Clamp to line segment
// Calculate closest point
var closestMapPoint = {
x: p1Map.x + t * dx,
y: p1Map.y + t * dy
};
var distance = Math.sqrt(
Math.pow(closestMapPoint.x - targetMapPoint.x, 2) +
Math.pow(closestMapPoint.x - targetMapPoint.x, 2) +
Math.pow(closestMapPoint.y - targetMapPoint.y, 2)
);
// Convert back to lat/lng
var closestLatLng = editor.map.containerPointToLatLng(closestMapPoint);
return {
latlng: closestLatLng,
distance: distance
@ -1862,7 +1884,7 @@ editor = {
_show_snap_indicator: function(latlng) {
editor._clear_snap_indicators();
var indicator = L.circleMarker(latlng, {
radius: 4,
color: '#ff6b6b',
@ -1871,7 +1893,7 @@ editor = {
weight: 2,
className: 'snap-indicator'
});
editor._snap_indicator.addLayer(indicator);
},
@ -1879,6 +1901,566 @@ editor = {
if (editor._snap_indicator) {
editor._snap_indicator.clearLayers();
}
},
// Stairway Creator functionality
_stairway_creator_active: false,
_stairway_type: 'straight', // 'straight', 'u-shaped', 'c-shaped'
_stairway_step_width: 0.3,
_stairway_step_depth: 0.25,
_stairway_steps_count: 10,
_stairway_points: [],
_stairway_preview_layer: null,
_stairway_control: null,
init_stairway_creator: function() {
// Initialize stairway creator preview layer
editor._stairway_preview_layer = L.layerGroup().addTo(editor.map);
// Add stairway creator control
editor._add_stairway_controls();
},
_add_stairway_controls: function() {
// Add stairway creator control to the map
var stairwayControl = L.control({position: 'topleft'});
stairwayControl.onAdd = function() {
var container = L.DomUtil.create('div', 'leaflet-bar leaflet-control leaflet-control-stairway');
container.innerHTML = '<a href="#" title="Stairway Creator" class="stairway-toggle">🏗️</a>';
L.DomEvent.on(container.querySelector('.stairway-toggle'), 'click', function(e) {
e.preventDefault();
editor._toggle_stairway_creator();
});
L.DomEvent.disableClickPropagation(container);
return container;
};
editor._stairway_control = stairwayControl.addTo(editor.map);
},
_toggle_stairway_creator: function() {
if (editor._stairway_creator_active) {
editor._deactivate_stairway_creator();
} else {
editor._activate_stairway_creator();
}
},
_activate_stairway_creator: function() {
// Activate for any geometry creation to help with stair drawing
if (!editor._creating) {
alert('Please start creating a geometry first.');
return;
}
editor._stairway_creator_active = true;
editor._stairway_points = [];
// Update UI
var toggle = document.querySelector('.stairway-toggle');
if (toggle) {
toggle.classList.add('active');
}
// Add stairway configuration panel to sidebar
editor._add_stairway_config_panel();
// Override map click handler for stairway creation
editor.map.off('click', editor._original_map_click);
editor.map.on('click', editor._handle_stairway_click);
// Show instructions
editor._show_stairway_instructions();
},
_deactivate_stairway_creator: function() {
editor._stairway_creator_active = false;
editor._stairway_points = [];
// Update UI
var toggle = document.querySelector('.stairway-toggle');
if (toggle) {
toggle.classList.remove('active');
}
// Remove configuration panel
editor._remove_stairway_config_panel();
// Clear preview
editor._clear_stairway_preview();
// Restore original map click handler
editor.map.off('click', editor._handle_stairway_click);
// Note: Let the normal creation process handle clicks
// Hide instructions
editor._hide_stairway_instructions();
},
_add_stairway_config_panel: function() {
var sidebar = $('#sidebar .content');
var configHtml = `
<div id="stairway-config" class="form-group">
<h4>Stairway Creator</h4>
<div class="form-group">
<label for="stairway-type">Stair Type:</label>
<select id="stairway-type" class="form-control">
<option value="straight">Straight</option>
</select>
<small class="form-text text-muted">U-Shaped and C-Shaped stairs coming soon!</small>
</div>
<div class="form-group">
<label for="stairway-steps">Number of Steps:</label>
<input type="number" id="stairway-steps" class="form-control"
value="10" min="3" max="50">
</div>
<div class="form-group">
<label for="stairway-step-width">Step Width (m):</label>
<input type="number" id="stairway-step-width" class="form-control"
value="0.3" min="0.1" max="2.0" step="0.1">
</div>
<div class="form-group">
<label for="stairway-step-depth">Step Depth (m):</label>
<input type="number" id="stairway-step-depth" class="form-control"
value="0.25" min="0.1" max="1.0" step="0.05">
</div>
<div class="btn-group">
<button type="button" id="generate-stairway" class="btn btn-primary">Generate Stairs</button>
<button type="button" id="cancel-stairway" class="btn btn-default">Cancel</button>
</div>
</div>
`;
sidebar.prepend(configHtml);
// Bind events
$('#stairway-type').on('change', function() {
editor._stairway_type = $(this).val();
editor._update_stairway_preview();
});
$('#stairway-steps').on('input', function() {
editor._stairway_steps_count = parseInt($(this).val()) || 10;
if (editor._stairway_points.length >= 2) {
editor._update_stairway_preview();
}
});
$('#stairway-step-width').on('input', function() {
editor._stairway_step_width = parseFloat($(this).val()) || 0.3;
if (editor._stairway_points.length >= 2) {
editor._update_stairway_preview();
}
});
$('#stairway-step-depth').on('input', function() {
editor._stairway_step_depth = parseFloat($(this).val()) || 0.25;
if (editor._stairway_points.length >= 2) {
editor._update_stairway_preview();
}
});
$('#generate-stairway').on('click', editor._generate_stairway);
$('#cancel-stairway').on('click', editor._deactivate_stairway_creator);
},
_remove_stairway_config_panel: function() {
$('#stairway-config').remove();
},
_show_stairway_instructions: function() {
var instructions = `
<div id="stairway-instructions" class="alert alert-info">
<strong>Stairway Creator Instructions:</strong><br>
Click two points to define the start and end of the straight stairway.<br>
Individual step geometries will be created perpendicular to this path.
</div>
`;
$('#sidebar .content').prepend(instructions);
},
_hide_stairway_instructions: function() {
$('#stairway-instructions').remove();
},
_handle_stairway_click: function(e) {
if (!editor._stairway_creator_active) return;
editor._stairway_points.push([e.latlng.lat, e.latlng.lng]);
var requiredPoints = 2; // Always 2 points for straight stairs
if (editor._stairway_points.length === requiredPoints) {
editor._update_stairway_preview();
$('#generate-stairway').prop('disabled', false);
} else if (editor._stairway_points.length < requiredPoints) {
// Show preview point
editor._show_stairway_point_preview();
$('#generate-stairway').prop('disabled', true);
}
// Update instructions
var remaining = requiredPoints - editor._stairway_points.length;
if (remaining > 0) {
$('#stairway-instructions').html(`
<strong>Stairway Creator:</strong><br>
Click ${remaining} more point${remaining > 1 ? 's' : ''} to complete the stairway path.
`);
} else {
$('#stairway-instructions').html(`
<strong>Stairway Creator:</strong><br>
Path complete! Adjust settings and click "Generate Stairs".
`);
}
},
_show_stairway_point_preview: function() {
editor._clear_stairway_preview();
// Show clicked points
editor._stairway_points.forEach(function(point, index) {
var marker = L.circleMarker([point[0], point[1]], {
radius: 6,
color: '#ff6b6b',
fillColor: '#ff6b6b',
fillOpacity: 0.8,
weight: 2
}).addTo(editor._stairway_preview_layer);
// Add point labels
var label = L.marker([point[0], point[1]], {
icon: L.divIcon({
html: String(index + 1),
className: 'stairway-point-label',
iconSize: [20, 20]
})
}).addTo(editor._stairway_preview_layer);
});
},
_update_stairway_preview: function() {
if (editor._stairway_points.length === 0) return;
editor._clear_stairway_preview();
editor._show_stairway_point_preview();
var requiredPoints = 2; // Always 2 points for straight stairs
if (editor._stairway_points.length < requiredPoints) return;
// Generate and show step preview
var steps = editor._calculate_stairway_steps();
steps.forEach(function(step, index) {
var stepLine = L.polyline(step, {
color: '#4CAF50',
weight: 3,
opacity: 0.7,
className: 'stairway-step-preview'
}).addTo(editor._stairway_preview_layer);
// Add step number
var midPoint = editor._getMidPoint(step[0], step[1]);
var stepLabel = L.marker(midPoint, {
icon: L.divIcon({
html: String(index + 1),
className: 'stairway-step-label',
iconSize: [16, 16]
})
}).addTo(editor._stairway_preview_layer);
});
},
_calculate_stairway_steps: function() {
var steps = [];
var points = editor._stairway_points;
if (editor._stairway_type === 'straight') {
steps = editor._calculate_straight_stairs(points[0], points[1]);
} else if (editor._stairway_type === 'u-shaped') {
steps = editor._calculate_u_shaped_stairs(points[0], points[1], points[2]);
} else if (editor._stairway_type === 'c-shaped') {
steps = editor._calculate_c_shaped_stairs(points[0], points[1], points[2]);
}
return steps;
},
_calculate_straight_stairs: function(start, end) {
var steps = [];
var stepCount = editor._stairway_steps_count;
var stepWidth = editor._stairway_step_width;
// Calculate direction vector
var dx = end[1] - start[1];
var dy = end[0] - start[0];
var length = Math.sqrt(dx * dx + dy * dy);
// Normalize direction
var dirX = dx / length;
var dirY = dy / length;
// Calculate perpendicular vector for step width
var perpX = -dirY * stepWidth / 111320; // Approximate meters to degrees
var perpY = dirX * stepWidth / (111320 * Math.cos(start[0] * Math.PI / 180));
// Calculate step positions along the path
for (var i = 0; i < stepCount; i++) {
var t = i / (stepCount - 1);
var centerLat = start[0] + t * dy;
var centerLng = start[1] + t * dx;
// Create step line perpendicular to path
var stepStart = [centerLat + perpY / 2, centerLng + perpX / 2];
var stepEnd = [centerLat - perpY / 2, centerLng - perpX / 2];
steps.push([stepStart, stepEnd]);
}
return steps;
},
_calculate_u_shaped_stairs: function(start, turn, end) {
var steps = [];
var stepCount = editor._stairway_steps_count;
var stepsPerSegment = Math.floor(stepCount / 2);
// First segment: start to turn
var segment1Steps = editor._calculate_straight_stairs(start, turn);
steps = steps.concat(segment1Steps.slice(0, stepsPerSegment));
// Second segment: turn to end
var segment2Steps = editor._calculate_straight_stairs(turn, end);
steps = steps.concat(segment2Steps.slice(0, stepCount - stepsPerSegment));
return steps;
},
_calculate_c_shaped_stairs: function(start, center, end) {
var steps = [];
var stepCount = editor._stairway_steps_count;
var stepWidth = editor._stairway_step_width;
// Calculate radius and angles for curved path
var radius = editor._distance(center, start);
var startAngle = Math.atan2(start[0] - center[0], start[1] - center[1]);
var endAngle = Math.atan2(end[0] - center[0], end[1] - center[1]);
// Ensure we go the shorter way around
var angleDiff = endAngle - startAngle;
if (angleDiff > Math.PI) angleDiff -= 2 * Math.PI;
if (angleDiff < -Math.PI) angleDiff += 2 * Math.PI;
for (var i = 0; i < stepCount; i++) {
var t = i / (stepCount - 1);
var angle = startAngle + t * angleDiff;
// Point on the curve
var curveLat = center[0] + radius * Math.sin(angle);
var curveLng = center[1] + radius * Math.cos(angle);
// Calculate tangent direction
var tangentAngle = angle + Math.PI / 2;
var perpLat = stepWidth * Math.cos(tangentAngle) / 111320;
var perpLng = stepWidth * Math.sin(tangentAngle) / (111320 * Math.cos(curveLat * Math.PI / 180));
// Create step line
var stepStart = [curveLat + perpLat / 2, curveLng + perpLng / 2];
var stepEnd = [curveLat - perpLat / 2, curveLng - perpLng / 2];
steps.push([stepStart, stepEnd]);
}
return steps;
},
_generate_stairway: function() {
if (editor._stairway_points.length < 2) {
alert('Please click two points to define the stairway path.');
return;
}
// Calculate step positions
var steps = editor._calculate_straight_stairs(
editor._stairway_points[0],
editor._stairway_points[1]
);
if (steps.length === 0) {
alert('Could not generate step positions. Please try different points.');
return;
}
// Create individual Stair database objects instead of visual lines
editor._create_individual_stairs(steps);
// Deactivate stairway creator
editor._deactivate_stairway_creator();
},
_create_individual_stairs: function(steps) {
// Get current space ID from the form
var spaceId = $('#sidebar form').attr('data-space-id') || editor._current_space_id;
if (!spaceId) {
alert('Could not determine current space ID. Please try again.');
return;
}
// Create individual Stair objects via sequential API calls
var createdStairs = 0;
var totalStairs = steps.length;
function createNextStair(index) {
if (index >= totalStairs) {
// All stairs created, refresh the view
alert(`Successfully created ${createdStairs} individual stairs. Refreshing view...`);
window.location.reload();
return;
}
var step = steps[index];
var lineStringGeometry = {
type: 'LineString',
coordinates: step.map(function(point) {
return [point[1], point[0]]; // Convert lat,lng to lng,lat for GeoJSON
})
};
// Create the Stair object
var stairData = {
space: spaceId,
geometry: lineStringGeometry
};
// Make API call to create individual Stair
fetch('/editor/stair/create/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': $('[name=csrfmiddlewaretoken]').val()
},
body: JSON.stringify(stairData)
})
.then(response => {
if (response.ok) {
createdStairs++;
} else {
console.error(`Failed to create stair ${index + 1}:`, response.statusText);
}
// Continue with next stair
createNextStair(index + 1);
})
.catch(error => {
console.error(`Error creating stair ${index + 1}:`, error);
// Continue with next stair even if this one failed
createNextStair(index + 1);
});
}
// Start creating stairs
createNextStair(0);
},
_update_geometry_form_with_steps: function(steps) {
// Create a MultiLineString geometry containing all steps
var coordinates = steps.map(function(step) {
return step.map(function(point) {
return [point[1], point[0]]; // Convert [lat, lng] to [lng, lat] for GeoJSON
});
});
var geometry = {
type: "MultiLineString",
coordinates: coordinates
};
// Update the form
$('#id_geometry').val(JSON.stringify(geometry));
},
_clear_stairway_preview: function() {
if (editor._stairway_preview_layer) {
editor._stairway_preview_layer.clearLayers();
}
},
_clear_stairway_creator_state: function() {
if (editor._stairway_creator_active) {
editor._deactivate_stairway_creator();
}
},
// Helper functions for stairway calculations
_getMidPoint: function(point1, point2) {
return [(point1[0] + point2[0]) / 2, (point1[1] + point2[1]) / 2];
},
_distance: function(point1, point2) {
var dx = point2[1] - point1[1];
var dy = point2[0] - point1[0];
return Math.sqrt(dx * dx + dy * dy);
},
_is_creating_stair: function() {
// Primary method: Check stored model type (most reliable)
if (editor._creating_model_type === 'stair') {
return true;
}
// Fallback methods for robustness
// Method 1: Check form data-new attribute
var form = $('#sidebar form[data-new]');
if (form.length && form.attr('data-new') === 'stair') {
return true;
}
// Method 2: Check URL path
var path = window.location.pathname;
if (path.indexOf('/stairs/') !== -1 && path.indexOf('/create') !== -1) {
return true;
}
// Method 3: Check if we're in a stairs context by looking at the page title or headers
var pageTitle = $('#sidebar h3, #sidebar h4').text().toLowerCase();
if (pageTitle.indexOf('stair') !== -1) {
return true;
}
return false;
},
_get_current_space_id: function() {
// Multiple methods to get the current space ID
// Method 1: Check URL path for space ID
var path = window.location.pathname;
var spaceMatch = path.match(/\/space\/(\d+)\//);
if (spaceMatch) {
return parseInt(spaceMatch[1]);
}
// Method 2: Check form or data attributes
var spaceInput = $('#sidebar').find('input[name="space"]');
if (spaceInput.length && spaceInput.val()) {
return parseInt(spaceInput.val());
}
// Method 3: Check if there's a space context in the page
var spaceId = $('#sidebar').find('[data-space-id]').attr('data-space-id');
if (spaceId) {
return parseInt(spaceId);
}
// Method 4: Extract from current geometry URL
if (editor._last_geometry_url && editor._last_geometry_url.indexOf('/space/') !== -1) {
var urlMatch = editor._last_geometry_url.match(/\/space\/(\d+)\//);
if (urlMatch) {
return parseInt(urlMatch[1]);
}
}
return null;
}
};

View file

@ -0,0 +1,33 @@
# Generated by Django 5.1.5 on 2025-08-01 21:38
import c3nav.mapdata.fields
import django.core.validators
import django.db.models.deletion
from decimal import Decimal
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mapdata', '0138_rangingbeacon_max_observed_num_clients_and_more'),
]
operations = [
migrations.CreateModel(
name='Stairway',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('import_tag', models.CharField(blank=True, max_length=64, null=True, verbose_name='import tag')),
('geometry', c3nav.mapdata.fields.GeometryField(default=None, geomtype='polygon', help_text=None)),
('stair_count', models.PositiveIntegerField(default=5, validators=[django.core.validators.MinValueValidator(1)], verbose_name='number of stairs')),
('stair_width', models.DecimalField(decimal_places=2, default=0.3, max_digits=4, validators=[django.core.validators.MinValueValidator(Decimal('0.1'))], verbose_name='stair width')),
('space', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='mapdata.space', verbose_name='space')),
],
options={
'verbose_name': 'Stairway',
'verbose_name_plural': 'Stairways',
'default_related_name': 'stairways',
},
),
]

View file

@ -3,7 +3,7 @@ from c3nav.mapdata.models.access import AccessRestriction # noqa
from c3nav.mapdata.models.altitudes import GroundAltitude # noqa
from c3nav.mapdata.models.level import Level # noqa
from c3nav.mapdata.models.geometry.level import Building, Space, Door, AltitudeArea # noqa
from c3nav.mapdata.models.geometry.space import Area, Stair, Obstacle, LineObstacle, Hole, AltitudeMarker # noqa
from c3nav.mapdata.models.geometry.space import Area, Stair, Stairway, Obstacle, LineObstacle, Hole, AltitudeMarker # noqa
from c3nav.mapdata.models.locations import Location, LocationSlug, LocationGroup, LocationGroupCategory # noqa
from c3nav.mapdata.models.source import Source # noqa
from c3nav.mapdata.models.graph import GraphNode, WayType, GraphEdge # noqa

View file

@ -172,6 +172,22 @@ class Stair(SpaceGeometryMixin, models.Model):
default_related_name = 'stairs'
class Stairway(SpaceGeometryMixin, models.Model):
"""
A stairway area that can generate individual stairs
"""
geometry = GeometryField('polygon')
stair_count = models.PositiveIntegerField(_('number of stairs'), default=5,
validators=[MinValueValidator(1)])
stair_width = models.DecimalField(_('stair width'), max_digits=4, decimal_places=2, default=0.3,
validators=[MinValueValidator(Decimal('0.1'))])
class Meta:
verbose_name = _('Stairway')
verbose_name_plural = _('Stairways')
default_related_name = 'stairways'
class Ramp(SpaceGeometryMixin, models.Model):
"""
A ramp

View file

@ -0,0 +1,18 @@
# Generated by Django 5.1.5 on 2025-08-01 21:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mesh', '0014_remove_meshnode_name'),
]
operations = [
migrations.AlterField(
model_name='nodemessage',
name='message_type',
field=models.CharField(choices=[('NOOP', 'noop'), ('ECHO_REQUEST', 'echo request'), ('ECHO_RESPONSE', 'echo response'), ('MESH_SIGNIN', 'mesh signin'), ('MESH_LAYER_ANNOUNCE', 'mesh layer announce'), ('MESH_ADD_DESTINATION', 'mesh add destination'), ('MESH_REMOVE_DESTINATIONS', 'mesh remove destinations'), ('MESH_ROUTE_REQUEST', 'mesh route request'), ('MESH_ROUTE_RESPONSE', 'mesh route response'), ('MESH_ROUTE_TRACE', 'mesh route trace'), ('MESH_ROUTING_FAILED', 'mesh routing failed'), ('MESH_SIGNIN_CONFIRM', 'mesh signin confirm'), ('MESH_RESET', 'mesh reset'), ('CONFIG_DUMP', 'dump config'), ('CONFIG_HARDWARE', 'hardware config'), ('CONFIG_BOARD', 'board config'), ('CONFIG_FIRMWARE', 'firmware config'), ('CONFIG_UPLINK', 'uplink config'), ('CONFIG_POSITION', 'position config'), ('CONFIG_NODE', 'node config'), ('CONFIG_IBEACON', 'ibeacon config'), ('OTA_STATUS', 'ota status'), ('OTA_REQUEST_STATUS', 'ota request status'), ('OTA_START', 'ota start'), ('OTA_URL', 'ota url'), ('OTA_FRAGMENT', 'ota fragment'), ('OTA_REQUEST_FRAGMENTS', 'ota request fragments'), ('OTA_SETTING', 'ota setting'), ('OTA_APPLY', 'ota apply'), ('OTA_ABORT', 'ota abort'), ('LOCATE_REQUEST_RANGE', 'locate request range'), ('LOCATE_RANGE_RESULTS', 'locate range results'), ('LOCATE_RAW_FTM_RESULTS', 'locate raw ftm results'), ('REBOOT', 'reboot'), ('REPORT_ERROR', 'report error')], db_index=True, max_length=24, verbose_name='message type'),
),
]