wip stairs impl
This commit is contained in:
parent
603329fbda
commit
2c840e5e20
9 changed files with 941 additions and 43 deletions
|
@ -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',
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
33
src/c3nav/mapdata/migrations/0139_stairway.py
Normal file
33
src/c3nav/mapdata/migrations/0139_stairway.py
Normal 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',
|
||||
},
|
||||
),
|
||||
]
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'),
|
||||
),
|
||||
]
|
Loading…
Add table
Add a link
Reference in a new issue