Added extra functionality to geojson viewer (search and zoom), plus label fixes
This commit is contained in:
parent
0a19473292
commit
89588201bd
|
|
@ -766,6 +766,14 @@
|
||||||
|
|
||||||
<div class="features-section" id="featuresSection" style="display: none;">
|
<div class="features-section" id="featuresSection" style="display: none;">
|
||||||
<h2>🗺️ Features</h2>
|
<h2>🗺️ Features</h2>
|
||||||
|
<div style="margin-bottom: 15px;">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="featureSearch"
|
||||||
|
placeholder="Search by field code..."
|
||||||
|
style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 0.9em; box-sizing: border-box;"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div class="features-list" id="featuresList"></div>
|
<div class="features-list" id="featuresList"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -791,18 +799,11 @@
|
||||||
<div class="map-controls-group">
|
<div class="map-controls-group">
|
||||||
<label for="labelsToggle">
|
<label for="labelsToggle">
|
||||||
<div class="toggle-switch">
|
<div class="toggle-switch">
|
||||||
<input type="checkbox" id="labelsToggle" checked>
|
<input type="checkbox" id="labelsToggle">
|
||||||
<span class="toggle-slider"></span>
|
<span class="toggle-slider"></span>
|
||||||
</div>
|
</div>
|
||||||
<span>Show Labels</span>
|
<span>Show Labels</span>
|
||||||
</label>
|
</label>
|
||||||
<label for="labelPositionToggle">
|
|
||||||
<div class="toggle-switch">
|
|
||||||
<input type="checkbox" id="labelPositionToggle">
|
|
||||||
<span class="toggle-slider"></span>
|
|
||||||
</div>
|
|
||||||
<span id="labelPositionText">Center</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -842,8 +843,7 @@
|
||||||
let labelsLayer = null;
|
let labelsLayer = null;
|
||||||
let currentGeojsonData = null;
|
let currentGeojsonData = null;
|
||||||
let featuresList = [];
|
let featuresList = [];
|
||||||
let showLabels = true;
|
let showLabels = false;
|
||||||
let labelPosition = 'center'; // 'center' or 'side'
|
|
||||||
|
|
||||||
// Toggle map layer
|
// Toggle map layer
|
||||||
const mapToggle = document.getElementById('mapToggle');
|
const mapToggle = document.getElementById('mapToggle');
|
||||||
|
|
@ -869,24 +869,16 @@
|
||||||
const labelsToggle = document.getElementById('labelsToggle');
|
const labelsToggle = document.getElementById('labelsToggle');
|
||||||
labelsToggle.addEventListener('change', () => {
|
labelsToggle.addEventListener('change', () => {
|
||||||
showLabels = labelsToggle.checked;
|
showLabels = labelsToggle.checked;
|
||||||
if (labelsLayer) {
|
if (!labelsLayer) {
|
||||||
if (showLabels) {
|
console.warn('Labels layer not initialized');
|
||||||
labelsLayer.addTo(map);
|
return;
|
||||||
} else {
|
|
||||||
map.removeLayer(labelsLayer);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
if (showLabels) {
|
||||||
// Toggle label position
|
labelsLayer.addTo(map);
|
||||||
const labelPositionToggle = document.getElementById('labelPositionToggle');
|
updateLabelsVisibility();
|
||||||
const labelPositionText = document.getElementById('labelPositionText');
|
} else {
|
||||||
labelPositionToggle.addEventListener('change', () => {
|
map.removeLayer(labelsLayer);
|
||||||
labelPosition = labelPositionToggle.checked ? 'side' : 'center';
|
|
||||||
labelPositionText.textContent = labelPositionToggle.checked ? 'Side' : 'Center';
|
|
||||||
// Recreate labels with new position
|
|
||||||
if (currentGeojsonData) {
|
|
||||||
loadGeojson(currentGeojsonData, '');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -914,6 +906,7 @@
|
||||||
const totalFieldsEl = document.getElementById('totalFields');
|
const totalFieldsEl = document.getElementById('totalFields');
|
||||||
const totalHectaresEl = document.getElementById('totalHectares');
|
const totalHectaresEl = document.getElementById('totalHectares');
|
||||||
const totalAcresEl = document.getElementById('totalAcres');
|
const totalAcresEl = document.getElementById('totalAcres');
|
||||||
|
const featureSearch = document.getElementById('featureSearch');
|
||||||
|
|
||||||
// Show message
|
// Show message
|
||||||
function showMessage(message, type = 'error') {
|
function showMessage(message, type = 'error') {
|
||||||
|
|
@ -923,64 +916,6 @@
|
||||||
setTimeout(() => el.classList.remove('active'), 5000);
|
setTimeout(() => el.classList.remove('active'), 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve label overlaps using collision detection algorithm
|
|
||||||
function resolveLabelOverlaps(labelPositions) {
|
|
||||||
const labelWidth = 150; // Approximate label width in pixels
|
|
||||||
const labelHeight = 30; // Approximate label height in pixels
|
|
||||||
const minDistance = Math.sqrt(labelWidth * labelWidth + labelHeight * labelHeight);
|
|
||||||
|
|
||||||
const adjustedPositions = labelPositions.map(pos => ({ ...pos }));
|
|
||||||
|
|
||||||
// Iteratively adjust positions to avoid overlaps
|
|
||||||
for (let iteration = 0; iteration < 5; iteration++) {
|
|
||||||
let hasAdjustment = false;
|
|
||||||
|
|
||||||
for (let i = 0; i < adjustedPositions.length; i++) {
|
|
||||||
for (let j = i + 1; j < adjustedPositions.length; j++) {
|
|
||||||
const pos1 = adjustedPositions[i];
|
|
||||||
const pos2 = adjustedPositions[j];
|
|
||||||
|
|
||||||
const p1 = map.project(pos1.latlng);
|
|
||||||
const p2 = map.project(pos2.latlng);
|
|
||||||
|
|
||||||
const dx = p2.x - p1.x;
|
|
||||||
const dy = p2.y - p1.y;
|
|
||||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
||||||
|
|
||||||
// If labels are too close, push them apart
|
|
||||||
if (distance < minDistance && distance > 0) {
|
|
||||||
hasAdjustment = true;
|
|
||||||
const angle = Math.atan2(dy, dx);
|
|
||||||
const pushDistance = (minDistance - distance) / 2 + 5;
|
|
||||||
|
|
||||||
// Move labels away from each other
|
|
||||||
const offset1 = map.unproject([
|
|
||||||
p1.x - Math.cos(angle) * pushDistance,
|
|
||||||
p1.y - Math.sin(angle) * pushDistance
|
|
||||||
]);
|
|
||||||
const offset2 = map.unproject([
|
|
||||||
p2.x + Math.cos(angle) * pushDistance,
|
|
||||||
p2.y + Math.sin(angle) * pushDistance
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Only adjust if original feature hasn't moved too far
|
|
||||||
const maxDrift = 0.003;
|
|
||||||
if (Math.abs(offset1.lat - pos1.originalLatlng.lat) < maxDrift) {
|
|
||||||
pos1.latlng = offset1;
|
|
||||||
}
|
|
||||||
if (Math.abs(offset2.lat - pos2.originalLatlng.lat) < maxDrift) {
|
|
||||||
pos2.latlng = offset2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasAdjustment) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return adjustedPositions;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate area in square meters using Turf.js
|
// Calculate area in square meters using Turf.js
|
||||||
function getFeatureArea(feature) {
|
function getFeatureArea(feature) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -1036,6 +971,98 @@
|
||||||
return { hectares, acres };
|
return { hectares, acres };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get first property key (field code)
|
||||||
|
function getFirstPropertyValue(properties) {
|
||||||
|
if (!properties) return '';
|
||||||
|
const keys = Object.keys(properties);
|
||||||
|
if (keys.length === 0) return '';
|
||||||
|
return properties[keys[0]];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create labels layer from features
|
||||||
|
function createLabelsLayer(features) {
|
||||||
|
labelsLayer = L.featureGroup([]);
|
||||||
|
|
||||||
|
// First pass: collect all label positions
|
||||||
|
const labelPositions = [];
|
||||||
|
features.forEach((feature, index) => {
|
||||||
|
const props = feature.properties || {};
|
||||||
|
const fieldCode = getFirstPropertyValue(props);
|
||||||
|
const displayLabel = fieldCode || `Feature ${index + 1}`;
|
||||||
|
|
||||||
|
if (feature.geometry && feature.geometry.type !== 'Point') {
|
||||||
|
// Get centroid for polygon features
|
||||||
|
const bounds = L.geoJSON(feature).getBounds();
|
||||||
|
const center = bounds.getCenter();
|
||||||
|
|
||||||
|
labelPositions.push({
|
||||||
|
fieldName: displayLabel,
|
||||||
|
latlng: center,
|
||||||
|
originalLatlng: center,
|
||||||
|
iconAnchor: [0, 0],
|
||||||
|
isPoint: false
|
||||||
|
});
|
||||||
|
} else if (feature.geometry && feature.geometry.type === 'Point') {
|
||||||
|
// For points, add label above the marker
|
||||||
|
const latlng = L.GeoJSON.coordsToLatLng(feature.geometry.coordinates);
|
||||||
|
labelPositions.push({
|
||||||
|
fieldName: displayLabel,
|
||||||
|
latlng: latlng,
|
||||||
|
originalLatlng: latlng,
|
||||||
|
iconAnchor: [0, 30],
|
||||||
|
isPoint: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create markers
|
||||||
|
labelPositions.forEach((pos) => {
|
||||||
|
const label = L.marker(pos.latlng, {
|
||||||
|
icon: L.divIcon({
|
||||||
|
className: 'field-label',
|
||||||
|
html: `<div style="
|
||||||
|
background: rgba(102, 126, 234, 0.9);
|
||||||
|
color: white;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||||
|
border: 2px solid white;
|
||||||
|
pointer-events: none;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 200px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
">${pos.fieldName}</div>`,
|
||||||
|
iconSize: [null, null],
|
||||||
|
iconAnchor: pos.iconAnchor
|
||||||
|
}),
|
||||||
|
interactive: false
|
||||||
|
});
|
||||||
|
labelsLayer.addLayer(label);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update labels visibility based on zoom level
|
||||||
|
function updateLabelsVisibility() {
|
||||||
|
const zoomLevel = map.getZoom();
|
||||||
|
const minZoomForLabels = 16; // Only show labels at zoom level 13 and above
|
||||||
|
|
||||||
|
if (labelsLayer && showLabels) {
|
||||||
|
if (zoomLevel >= minZoomForLabels) {
|
||||||
|
labelsLayer.eachLayer(layer => {
|
||||||
|
layer.setOpacity(1);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
labelsLayer.eachLayer(layer => {
|
||||||
|
layer.setOpacity(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Show message
|
// Show message
|
||||||
function showMessage(message, type = 'error') {
|
function showMessage(message, type = 'error') {
|
||||||
const el = type === 'error' ? errorMessage : successMessage;
|
const el = type === 'error' ? errorMessage : successMessage;
|
||||||
|
|
@ -1056,8 +1083,10 @@
|
||||||
reader.onload = (event) => {
|
reader.onload = (event) => {
|
||||||
try {
|
try {
|
||||||
const geojson = JSON.parse(event.target.result);
|
const geojson = JSON.parse(event.target.result);
|
||||||
loadGeojson(geojson, file.name);
|
const success = loadGeojson(geojson, file.name);
|
||||||
showMessage('GeoJSON loaded successfully!', 'success');
|
if (success) {
|
||||||
|
showMessage('GeoJSON loaded successfully!', 'success');
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showMessage(`Error parsing file: ${error.message}`, 'error');
|
showMessage(`Error parsing file: ${error.message}`, 'error');
|
||||||
}
|
}
|
||||||
|
|
@ -1068,8 +1097,57 @@
|
||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Validate CRS
|
||||||
|
function validateCRS(geojson) {
|
||||||
|
// If no CRS is specified, assume WGS84 (default for GeoJSON)
|
||||||
|
if (!geojson.crs) {
|
||||||
|
return { valid: true, message: 'No CRS specified (default WGS84)' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const crs = geojson.crs;
|
||||||
|
let crsName = '';
|
||||||
|
|
||||||
|
// Extract CRS name from different possible formats
|
||||||
|
if (crs.properties && crs.properties.name) {
|
||||||
|
crsName = crs.properties.name;
|
||||||
|
} else if (typeof crs === 'string') {
|
||||||
|
crsName = crs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's WGS84 (various possible names)
|
||||||
|
const wgs84Variations = [
|
||||||
|
'EPSG:4326',
|
||||||
|
'epsg:4326',
|
||||||
|
'urn:ogc:def:crs:EPSG::4326',
|
||||||
|
'urn:ogc:def:crs:EPSG:4.3:4326',
|
||||||
|
'WGS84',
|
||||||
|
'wgs84',
|
||||||
|
'urn:ogc:def:crs:OGC:1.3:CRS84'
|
||||||
|
];
|
||||||
|
|
||||||
|
const isWGS84 = wgs84Variations.some(variant =>
|
||||||
|
crsName.toUpperCase().includes(variant.toUpperCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isWGS84) {
|
||||||
|
return { valid: true, message: 'CRS: WGS84' };
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: `Non-supported CRS (${JSON.stringify(crs)})`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Load and display GeoJSON
|
// Load and display GeoJSON
|
||||||
function loadGeojson(geojson, fileName) {
|
function loadGeojson(geojson, fileName) {
|
||||||
|
// Validate CRS first
|
||||||
|
const crsValidation = validateCRS(geojson);
|
||||||
|
if (!crsValidation.valid) {
|
||||||
|
showMessage(crsValidation.message, 'error');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Clear previous layer
|
// Clear previous layer
|
||||||
if (geojsonLayer) {
|
if (geojsonLayer) {
|
||||||
map.removeLayer(geojsonLayer);
|
map.removeLayer(geojsonLayer);
|
||||||
|
|
@ -1090,7 +1168,7 @@
|
||||||
|
|
||||||
if (features.length === 0) {
|
if (features.length === 0) {
|
||||||
showMessage('GeoJSON contains no features', 'error');
|
showMessage('GeoJSON contains no features', 'error');
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create layer with popups
|
// Create layer with popups
|
||||||
|
|
@ -1142,89 +1220,16 @@
|
||||||
}).addTo(map);
|
}).addTo(map);
|
||||||
|
|
||||||
// Create labels layer
|
// Create labels layer
|
||||||
labelsLayer = L.featureGroup([]);
|
createLabelsLayer(features);
|
||||||
|
|
||||||
// First pass: collect all label positions
|
|
||||||
const labelPositions = [];
|
|
||||||
features.forEach((feature, index) => {
|
|
||||||
const props = feature.properties || {};
|
|
||||||
const fieldName = props.field || props.name || props.field_name || props.fieldName || props.id || `Field ${index + 1}`;
|
|
||||||
|
|
||||||
if (feature.geometry && feature.geometry.type !== 'Point') {
|
|
||||||
// Get centroid for polygon features
|
|
||||||
const bounds = L.geoJSON(feature).getBounds();
|
|
||||||
const center = bounds.getCenter();
|
|
||||||
|
|
||||||
let labelLatlng = center;
|
|
||||||
let iconAnchor = [0, 0];
|
|
||||||
|
|
||||||
// If side position, offset label to the top-left of the bounds
|
|
||||||
if (labelPosition === 'side') {
|
|
||||||
const ne = bounds.getNorthEast();
|
|
||||||
labelLatlng = L.latLng(ne.lat, ne.lng);
|
|
||||||
iconAnchor = [-10, -30];
|
|
||||||
}
|
|
||||||
|
|
||||||
labelPositions.push({
|
|
||||||
fieldName,
|
|
||||||
latlng: labelLatlng,
|
|
||||||
originalLatlng: labelLatlng,
|
|
||||||
iconAnchor,
|
|
||||||
isPoint: false
|
|
||||||
});
|
|
||||||
} else if (feature.geometry && feature.geometry.type === 'Point') {
|
|
||||||
// For points, add label above the marker
|
|
||||||
const latlng = L.GeoJSON.coordsToLatLng(feature.geometry.coordinates);
|
|
||||||
labelPositions.push({
|
|
||||||
fieldName,
|
|
||||||
latlng: latlng,
|
|
||||||
originalLatlng: latlng,
|
|
||||||
iconAnchor: [0, 30],
|
|
||||||
isPoint: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Apply collision detection to resolve overlaps
|
|
||||||
const adjustedPositions = resolveLabelOverlaps(labelPositions);
|
|
||||||
|
|
||||||
// Second pass: create markers with adjusted positions
|
|
||||||
adjustedPositions.forEach((pos) => {
|
|
||||||
const label = L.marker(pos.latlng, {
|
|
||||||
icon: L.divIcon({
|
|
||||||
className: 'field-label',
|
|
||||||
html: `<div style="
|
|
||||||
background: rgba(102, 126, 234, 0.9);
|
|
||||||
color: white;
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
white-space: nowrap;
|
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
|
||||||
border: 2px solid white;
|
|
||||||
pointer-events: none;
|
|
||||||
text-align: center;
|
|
||||||
max-width: 200px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
">${pos.fieldName}</div>`,
|
|
||||||
iconSize: [null, null],
|
|
||||||
iconAnchor: pos.iconAnchor
|
|
||||||
}),
|
|
||||||
interactive: false
|
|
||||||
});
|
|
||||||
labelsLayer.addLayer(label);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (showLabels) {
|
|
||||||
labelsLayer.addTo(map);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fit bounds
|
// Fit bounds
|
||||||
const bounds = geojsonLayer.getBounds();
|
const bounds = geojsonLayer.getBounds();
|
||||||
map.fitBounds(bounds, { padding: [50, 50] });
|
map.fitBounds(bounds, { padding: [50, 50] });
|
||||||
|
|
||||||
|
// Update labels visibility based on zoom level
|
||||||
|
updateLabelsVisibility();
|
||||||
|
map.on('zoom', updateLabelsVisibility);
|
||||||
|
|
||||||
// Update stats
|
// Update stats
|
||||||
updateStats(geojson, features);
|
updateStats(geojson, features);
|
||||||
|
|
||||||
|
|
@ -1243,6 +1248,8 @@
|
||||||
featuresSection.style.display = 'block';
|
featuresSection.style.display = 'block';
|
||||||
tableSection.style.display = 'block';
|
tableSection.style.display = 'block';
|
||||||
downloadBtn.style.display = 'block';
|
downloadBtn.style.display = 'block';
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update statistics
|
// Update statistics
|
||||||
|
|
@ -1268,14 +1275,19 @@
|
||||||
// Display features list
|
// Display features list
|
||||||
function displayFeaturesList() {
|
function displayFeaturesList() {
|
||||||
featureLst.innerHTML = '';
|
featureLst.innerHTML = '';
|
||||||
|
featureSearch.value = '';
|
||||||
|
|
||||||
featuresList.forEach((item, index) => {
|
featuresList.forEach((item, index) => {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'feature-item';
|
div.className = 'feature-item';
|
||||||
|
|
||||||
// Get feature name
|
// Get first property value (field code)
|
||||||
const props = item.feature.properties || {};
|
const props = item.feature.properties || {};
|
||||||
const name = props.name || props.id || `Feature ${index + 1}`;
|
const fieldCode = getFirstPropertyValue(props);
|
||||||
|
const name = fieldCode || `Feature ${index + 1}`;
|
||||||
|
|
||||||
div.textContent = name;
|
div.textContent = name;
|
||||||
|
div.dataset.index = index;
|
||||||
|
|
||||||
div.addEventListener('click', () => selectFeature(index));
|
div.addEventListener('click', () => selectFeature(index));
|
||||||
featureLst.appendChild(div);
|
featureLst.appendChild(div);
|
||||||
|
|
@ -1285,35 +1297,43 @@
|
||||||
// Select feature
|
// Select feature
|
||||||
function selectFeature(index) {
|
function selectFeature(index) {
|
||||||
const item = featuresList[index];
|
const item = featuresList[index];
|
||||||
if (!item) return;
|
if (!item) {
|
||||||
|
console.error('Feature not found at index:', index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Update feature list UI
|
// Update feature list UI
|
||||||
document.querySelectorAll('.feature-item').forEach((el, i) => {
|
document.querySelectorAll('.feature-item').forEach((el, i) => {
|
||||||
el.classList.toggle('active', i === index);
|
el.classList.toggle('active', i === index);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update map
|
|
||||||
item.layer.fire('click');
|
|
||||||
if (item.layer.setStyle) {
|
|
||||||
item.layer.setStyle({
|
|
||||||
weight: 3,
|
|
||||||
opacity: 1
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display properties in sidebar
|
// Display properties in sidebar
|
||||||
displayProperties(item.feature);
|
displayProperties(item.feature);
|
||||||
|
|
||||||
// Open modal with properties
|
// Open modal with properties
|
||||||
openFeatureModal(item.feature);
|
openFeatureModal(item.feature);
|
||||||
|
|
||||||
// Scroll to layer on map
|
// Zoom to feature on map
|
||||||
if (item.feature.geometry.type === 'Point') {
|
setTimeout(() => {
|
||||||
const latlng = item.layer.getLatLng();
|
try {
|
||||||
map.setView(latlng, map.getZoom());
|
if (item.feature.geometry.type === 'Point') {
|
||||||
} else {
|
const latlng = item.layer.getLatLng();
|
||||||
const bounds = L.geoJSON(item.feature).getBounds();
|
map.setView(latlng, 14);
|
||||||
map.fitBounds(bounds, { padding: [100, 100] });
|
} else {
|
||||||
|
const bounds = L.geoJSON(item.feature).getBounds();
|
||||||
|
map.fitBounds(bounds, { padding: [100, 100] });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error zooming to feature:', e);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// Update map styling
|
||||||
|
if (item.layer.setStyle) {
|
||||||
|
item.layer.setStyle({
|
||||||
|
weight: 3,
|
||||||
|
opacity: 1
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1496,6 +1516,10 @@
|
||||||
map.removeLayer(geojsonLayer);
|
map.removeLayer(geojsonLayer);
|
||||||
geojsonLayer = null;
|
geojsonLayer = null;
|
||||||
}
|
}
|
||||||
|
if (labelsLayer) {
|
||||||
|
map.removeLayer(labelsLayer);
|
||||||
|
labelsLayer = null;
|
||||||
|
}
|
||||||
currentGeojsonData = null;
|
currentGeojsonData = null;
|
||||||
featuresList = [];
|
featuresList = [];
|
||||||
geojsonInput.value = '';
|
geojsonInput.value = '';
|
||||||
|
|
@ -1526,6 +1550,21 @@
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
showMessage('GeoJSON downloaded!', 'success');
|
showMessage('GeoJSON downloaded!', 'success');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Search features
|
||||||
|
featureSearch.addEventListener('input', (e) => {
|
||||||
|
const searchTerm = e.target.value.toLowerCase();
|
||||||
|
const items = document.querySelectorAll('.feature-item');
|
||||||
|
|
||||||
|
items.forEach(item => {
|
||||||
|
const text = item.textContent.toLowerCase();
|
||||||
|
if (text.includes(searchTerm)) {
|
||||||
|
item.style.display = '';
|
||||||
|
} else {
|
||||||
|
item.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue