mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 01:01:39 -05:00
Set default size of a hexagon
This commit is contained in:
parent
b0f3289435
commit
f578288ac9
3 changed files with 68 additions and 26 deletions
|
|
@ -22,7 +22,7 @@ class Api::V1::Maps::HexagonsController < ApiController
|
||||||
private
|
private
|
||||||
|
|
||||||
def bbox_params
|
def bbox_params
|
||||||
params.permit(:min_lon, :min_lat, :max_lon, :max_lat, :hex_size)
|
params.permit(:min_lon, :min_lat, :max_lon, :max_lat, :hex_size, :viewport_width, :viewport_height)
|
||||||
end
|
end
|
||||||
|
|
||||||
def hexagon_params
|
def hexagon_params
|
||||||
|
|
|
||||||
|
|
@ -19,23 +19,23 @@ export class HexagonGrid {
|
||||||
minZoom: 8, // Don't show hexagons below this zoom level
|
minZoom: 8, // Don't show hexagons below this zoom level
|
||||||
...options
|
...options
|
||||||
};
|
};
|
||||||
|
|
||||||
this.hexagonLayer = null;
|
this.hexagonLayer = null;
|
||||||
this.loadingController = null; // For aborting requests
|
this.loadingController = null; // For aborting requests
|
||||||
this.lastBounds = null;
|
this.lastBounds = null;
|
||||||
this.isVisible = false;
|
this.isVisible = false;
|
||||||
|
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
// Create the hexagon layer group
|
// Create the hexagon layer group
|
||||||
this.hexagonLayer = L.layerGroup();
|
this.hexagonLayer = L.layerGroup();
|
||||||
|
|
||||||
// Bind map events
|
// Bind map events
|
||||||
this.map.on('moveend', this.debounce(this.onMapMove.bind(this), this.options.debounceDelay));
|
this.map.on('moveend', this.debounce(this.onMapMove.bind(this), this.options.debounceDelay));
|
||||||
this.map.on('zoomend', this.onZoomChange.bind(this));
|
this.map.on('zoomend', this.onZoomChange.bind(this));
|
||||||
|
|
||||||
// Initial load if within zoom range
|
// Initial load if within zoom range
|
||||||
if (this.shouldShowHexagons()) {
|
if (this.shouldShowHexagons()) {
|
||||||
this.show();
|
this.show();
|
||||||
|
|
@ -94,7 +94,7 @@ export class HexagonGrid {
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentBounds = this.map.getBounds();
|
const currentBounds = this.map.getBounds();
|
||||||
|
|
||||||
// Only reload if bounds have changed significantly
|
// Only reload if bounds have changed significantly
|
||||||
if (this.boundsChanged(currentBounds)) {
|
if (this.boundsChanged(currentBounds)) {
|
||||||
this.loadHexagons();
|
this.loadHexagons();
|
||||||
|
|
@ -187,11 +187,18 @@ export class HexagonGrid {
|
||||||
const startDate = urlParams.get('start_at');
|
const startDate = urlParams.get('start_at');
|
||||||
const endDate = urlParams.get('end_at');
|
const endDate = urlParams.get('end_at');
|
||||||
|
|
||||||
|
// Get viewport dimensions
|
||||||
|
const mapContainer = this.map.getContainer();
|
||||||
|
const viewportWidth = mapContainer.offsetWidth;
|
||||||
|
const viewportHeight = mapContainer.offsetHeight;
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
min_lon: bounds.getWest(),
|
min_lon: bounds.getWest(),
|
||||||
min_lat: bounds.getSouth(),
|
min_lat: bounds.getSouth(),
|
||||||
max_lon: bounds.getEast(),
|
max_lon: bounds.getEast(),
|
||||||
max_lat: bounds.getNorth()
|
max_lat: bounds.getNorth(),
|
||||||
|
viewport_width: viewportWidth,
|
||||||
|
viewport_height: viewportHeight
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add date parameters if they exist
|
// Add date parameters if they exist
|
||||||
|
|
@ -210,7 +217,7 @@ export class HexagonGrid {
|
||||||
}
|
}
|
||||||
|
|
||||||
const geojsonData = await response.json();
|
const geojsonData = await response.json();
|
||||||
|
|
||||||
// Clear existing hexagons and add new ones
|
// Clear existing hexagons and add new ones
|
||||||
this.clearHexagons();
|
this.clearHexagons();
|
||||||
this.addHexagonsToMap(geojsonData);
|
this.addHexagonsToMap(geojsonData);
|
||||||
|
|
@ -252,7 +259,7 @@ export class HexagonGrid {
|
||||||
|
|
||||||
// Calculate max point count for color scaling
|
// Calculate max point count for color scaling
|
||||||
const maxPoints = Math.max(...geojsonData.features.map(f => f.properties.point_count));
|
const maxPoints = Math.max(...geojsonData.features.map(f => f.properties.point_count));
|
||||||
|
|
||||||
const geoJsonLayer = L.geoJSON(geojsonData, {
|
const geoJsonLayer = L.geoJSON(geojsonData, {
|
||||||
style: (feature) => this.styleHexagonByData(feature, maxPoints),
|
style: (feature) => this.styleHexagonByData(feature, maxPoints),
|
||||||
onEachFeature: (feature, layer) => {
|
onEachFeature: (feature, layer) => {
|
||||||
|
|
@ -279,21 +286,22 @@ export class HexagonGrid {
|
||||||
styleHexagonByData(feature, maxPoints) {
|
styleHexagonByData(feature, maxPoints) {
|
||||||
const props = feature.properties;
|
const props = feature.properties;
|
||||||
const pointCount = props.point_count || 0;
|
const pointCount = props.point_count || 0;
|
||||||
|
|
||||||
// Calculate opacity based on point density (0.2 to 0.8)
|
// Calculate opacity based on point density (0.2 to 0.8)
|
||||||
const opacity = 0.2 + (pointCount / maxPoints) * 0.6;
|
const opacity = 0.2 + (pointCount / maxPoints) * 0.6;
|
||||||
|
|
||||||
// Calculate color based on density
|
// Calculate color based on density
|
||||||
let color = '#3388ff'; // Default blue
|
let color = '#3388ff'
|
||||||
if (pointCount > maxPoints * 0.7) {
|
// let color = '#3388ff'; // Default blue
|
||||||
color = '#d73027'; // High density - red
|
// if (pointCount > maxPoints * 0.7) {
|
||||||
} else if (pointCount > maxPoints * 0.4) {
|
// color = '#d73027'; // High density - red
|
||||||
color = '#fc8d59'; // Medium-high density - orange
|
// } else if (pointCount > maxPoints * 0.4) {
|
||||||
} else if (pointCount > maxPoints * 0.2) {
|
// color = '#fc8d59'; // Medium-high density - orange
|
||||||
color = '#fee08b'; // Medium density - yellow
|
// } else if (pointCount > maxPoints * 0.2) {
|
||||||
} else {
|
// color = '#fee08b'; // Medium density - yellow
|
||||||
color = '#91bfdb'; // Low density - light blue
|
// } else {
|
||||||
}
|
// color = '#91bfdb'; // Low density - light blue
|
||||||
|
// }
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fillColor: color,
|
fillColor: color,
|
||||||
|
|
@ -310,7 +318,7 @@ export class HexagonGrid {
|
||||||
buildPopupContent(props) {
|
buildPopupContent(props) {
|
||||||
const startDate = props.earliest_point ? new Date(props.earliest_point).toLocaleDateString() : 'N/A';
|
const startDate = props.earliest_point ? new Date(props.earliest_point).toLocaleDateString() : 'N/A';
|
||||||
const endDate = props.latest_point ? new Date(props.latest_point).toLocaleDateString() : 'N/A';
|
const endDate = props.latest_point ? new Date(props.latest_point).toLocaleDateString() : 'N/A';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div style="font-size: 12px; line-height: 1.4;">
|
<div style="font-size: 12px; line-height: 1.4;">
|
||||||
<h4 style="margin: 0 0 8px 0; color: #2c5aa0;">Hexagon Stats</h4>
|
<h4 style="margin: 0 0 8px 0; color: #2c5aa0;">Hexagon Stats</h4>
|
||||||
|
|
@ -356,7 +364,7 @@ export class HexagonGrid {
|
||||||
*/
|
*/
|
||||||
updateStyle(newStyle) {
|
updateStyle(newStyle) {
|
||||||
this.options.style = { ...this.options.style, ...newStyle };
|
this.options.style = { ...this.options.style, ...newStyle };
|
||||||
|
|
||||||
// Update existing hexagons
|
// Update existing hexagons
|
||||||
this.hexagonLayer.eachLayer((layer) => {
|
this.hexagonLayer.eachLayer((layer) => {
|
||||||
if (layer.setStyle) {
|
if (layer.setStyle) {
|
||||||
|
|
@ -400,4 +408,4 @@ export function createHexagonGrid(map, options = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default export
|
// Default export
|
||||||
export default HexagonGrid;
|
export default HexagonGrid;
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ class Maps::HexagonGrid
|
||||||
|
|
||||||
# Constants for configuration
|
# Constants for configuration
|
||||||
DEFAULT_HEX_SIZE = 500 # meters (center to edge)
|
DEFAULT_HEX_SIZE = 500 # meters (center to edge)
|
||||||
|
TARGET_HEX_EDGE_PX = 20 # pixels (edge length target)
|
||||||
MAX_HEXAGONS_PER_REQUEST = 5000
|
MAX_HEXAGONS_PER_REQUEST = 5000
|
||||||
MAX_AREA_KM2 = 250_000 # 500km x 500km
|
MAX_AREA_KM2 = 250_000 # 500km x 500km
|
||||||
|
|
||||||
|
|
@ -13,7 +14,7 @@ class Maps::HexagonGrid
|
||||||
class InvalidCoordinatesError < StandardError; end
|
class InvalidCoordinatesError < StandardError; end
|
||||||
class PostGISError < StandardError; end
|
class PostGISError < StandardError; end
|
||||||
|
|
||||||
attr_reader :min_lon, :min_lat, :max_lon, :max_lat, :hex_size, :user_id, :start_date, :end_date
|
attr_reader :min_lon, :min_lat, :max_lon, :max_lat, :hex_size, :user_id, :start_date, :end_date, :viewport_width, :viewport_height
|
||||||
|
|
||||||
validates :min_lon, :max_lon, inclusion: { in: -180..180 }
|
validates :min_lon, :max_lon, inclusion: { in: -180..180 }
|
||||||
validates :min_lat, :max_lat, inclusion: { in: -90..90 }
|
validates :min_lat, :max_lat, inclusion: { in: -90..90 }
|
||||||
|
|
@ -27,7 +28,9 @@ class Maps::HexagonGrid
|
||||||
@min_lat = params[:min_lat].to_f
|
@min_lat = params[:min_lat].to_f
|
||||||
@max_lon = params[:max_lon].to_f
|
@max_lon = params[:max_lon].to_f
|
||||||
@max_lat = params[:max_lat].to_f
|
@max_lat = params[:max_lat].to_f
|
||||||
@hex_size = params[:hex_size]&.to_f || DEFAULT_HEX_SIZE
|
@viewport_width = params[:viewport_width]&.to_f
|
||||||
|
@viewport_height = params[:viewport_height]&.to_f
|
||||||
|
@hex_size = calculate_dynamic_hex_size(params)
|
||||||
@user_id = params[:user_id]
|
@user_id = params[:user_id]
|
||||||
@start_date = params[:start_date]
|
@start_date = params[:start_date]
|
||||||
@end_date = params[:end_date]
|
@end_date = params[:end_date]
|
||||||
|
|
@ -59,6 +62,37 @@ class Maps::HexagonGrid
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def calculate_dynamic_hex_size(params)
|
||||||
|
# If viewport dimensions are provided, calculate hex_size for 20px edge length
|
||||||
|
if viewport_width && viewport_height && viewport_width > 0 && viewport_height > 0
|
||||||
|
# Calculate the geographic width of the bounding box in meters
|
||||||
|
avg_lat = (min_lat + max_lat) / 2
|
||||||
|
bbox_width_degrees = (max_lon - min_lon).abs
|
||||||
|
bbox_width_meters = bbox_width_degrees * 111_320 * Math.cos(avg_lat * Math::PI / 180)
|
||||||
|
|
||||||
|
# Calculate how many meters per pixel based on current viewport span (zoom-independent)
|
||||||
|
meters_per_pixel = bbox_width_meters / viewport_width
|
||||||
|
|
||||||
|
# For a regular hexagon, the edge length is approximately 0.866 times the radius (center to vertex)
|
||||||
|
# So if we want a 20px edge, we need: edge_length_meters = 20 * meters_per_pixel
|
||||||
|
# And radius = edge_length / 0.866
|
||||||
|
edge_length_meters = TARGET_HEX_EDGE_PX * meters_per_pixel
|
||||||
|
hex_radius_meters = edge_length_meters / 0.866
|
||||||
|
|
||||||
|
# Clamp to reasonable bounds to prevent excessive computation
|
||||||
|
calculated_size = hex_radius_meters.clamp(50, 10_000)
|
||||||
|
|
||||||
|
Rails.logger.debug "Dynamic hex size calculation: bbox_width=#{bbox_width_meters.round}m, viewport=#{viewport_width}px, meters_per_pixel=#{meters_per_pixel.round(2)}, hex_size=#{calculated_size.round}m"
|
||||||
|
|
||||||
|
calculated_size
|
||||||
|
else
|
||||||
|
# Fallback to provided hex_size or default
|
||||||
|
fallback_size = params[:hex_size]&.to_f || DEFAULT_HEX_SIZE
|
||||||
|
Rails.logger.debug "Using fallback hex size: #{fallback_size}m (no viewport dimensions provided)"
|
||||||
|
fallback_size
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def validate_bbox_order
|
def validate_bbox_order
|
||||||
errors.add(:base, 'min_lon must be less than max_lon') if min_lon >= max_lon
|
errors.add(:base, 'min_lon must be less than max_lon') if min_lon >= max_lon
|
||||||
errors.add(:base, 'min_lat must be less than max_lat') if min_lat >= max_lat
|
errors.add(:base, 'min_lat must be less than max_lat') if min_lat >= max_lat
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue