StorymapperArrow/Drawing.js

569 lines
19 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { getArrowPolygon } from "athena-utils/shape/Arrow.js";
import {  ARROW_BODY_STYLE_CONSTANT, ARROW_BODY_STYLE_LINEAR, ARROW_BODY_STYLE_EXPONENTIAL  } from "athena-utils/shape/Arrow.js";
import { getFrontline } from "athena-utils/shape/Frontline.js";
import { LEFT_SIDE, RIGHT_SIDE, BOTH_SIDES } from "athena-utils/shape/Frontline.js";
const ARROW = "arrow";
const FRONTLINE = "frontline";
const ADDITIONAL_SIDE = "additionalSide"; // used for Id in BOTH_SIDES style
const DEFAULT_ARROW_PARAMS = {
splineStep: 20,
offsetDistance: 12000,
calculation: ARROW_BODY_STYLE_LINEAR,
range: 1,
minValue: 0.1,
widthArrow: 5,
lengthArrow: 8,
paintOptions: {
"fill-color": "#0099ff",
"fill-outline-color": "#005588",
"fill-opacity": 0.6
}
};
const DEFAULT_FRONTLINE_PARAMS = {
splineStep: 0.08,
offsetDistance: 10000,
style: LEFT_SIDE,
protrusion: {
length: 15000,
startSize: 5000,
endSize: 500,
gap: 15000
},
paintOptionsLeft: {
"fill-color": "#00ff37",
"fill-outline-color": "#008809",
"fill-opacity": 0.6
},
paintOptionsRight: {
"fill-color": "#0011ff",
"fill-outline-color": "#002b88",
"fill-opacity": 0.6
}
};
let currentFeature = null;
let currentDrawStyle = ARROW;
const arrowParamsMap = new Map();
const frontlineParamsMap = new Map();
mapboxgl.accessToken = 'pk.eyJ1Ijoib3V0ZG9vcm1hcHBpbmdjb21wYW55IiwiYSI6ImNqYmh3cDdjYzNsMnozNGxsYzlvMmk2bTYifQ.QqcZ4LVoLWnXafXdjZxnZg';
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',
center: [10, 50],
zoom: 5
});
// Drawing visuals
const draw = new MapboxDraw({
displayControlsDefault: false,
controls: {
line_string: true
},
defaultMode: 'draw_line_string',
styles: [
// Main drawing line
{
id: 'gl-draw-line',
type: 'line',
filter: ['all', ['==', '$type', 'LineString'], ['==', 'active', 'true']],
layout: {
'line-cap': 'round',
'line-join': 'round'
},
paint: {
'line-color': 'rgb(0, 255, 0)',
'line-width': 4
}
},
// This makes the nonstop visible line invisible
{
id: 'gl-draw-line-inactive',
type: 'line',
filter: ['all', ['==', '$type', 'LineString'], ['==', 'deactive', 'false']],
layout: {
'line-cap': 'round',
'line-join': 'round'
},
paint: {
'line-color': 'rgba(0, 0, 0, 0)',
'line-width': 2
}
},
// Selected point
{
id: 'gl-draw-polygon-and-line-vertex-active',
type: 'circle',
filter: ['all', ['==', '$type', 'Point'], ['==', 'meta', 'vertex'], ['==', 'active', 'true']],
paint: {
'circle-radius': 6,
'circle-color': 'rgb(255, 255, 0)'
}
},
// Main points
{
id: 'gl-draw-polygon-and-line-vertex-inactive',
type: 'circle',
filter: ['all', ['==', '$type', 'Point'], ['==', 'meta', 'vertex'], ['==', 'active', 'false']],
paint: {
'circle-radius': 4,
'circle-color': 'rgb(0, 255, 0)'
}
},
// Midpoints
{
id: 'gl-draw-polygon-midpoint',
type: 'circle',
filter: ['all', ['==', '$type', 'Point'], ['==', 'meta', 'midpoint']],
paint: {
'circle-radius': 4,
'circle-color': 'rgb(0, 255, 174)'
}
}
]
});
// Map controlls
map.addControl(draw, 'top-left');
map.on('draw.create', handleDraw);
map.on('draw.update', handleDraw);
map.on('click', (e) => {
hideArrowEditor();
hideFrontlineEditor();
handleClick(e, ARROW, arrowParamsMap, showArrowEditor);
handleClick(e, FRONTLINE, frontlineParamsMap, showFrontlineEditor);
});
map.dragPan.enable();
function handleDraw(e) {
const feature = e.features[0];
const coords = feature.geometry.coordinates;
const id = feature.id;
if (currentDrawStyle === ARROW) {
DrawArrow(coords, id);
} else if (currentDrawStyle === FRONTLINE) {
DrawFrontline(coords, id);
}
}
function handleClick(click, prefix, paramsMap, showEditorFunction) {
const features = map.queryRenderedFeatures(click.point, {
layers: map.getStyle().layers
.filter(l => l.id.startsWith(prefix + "-"))
.map(l => l.id)
});
if (features.length === 0) return;
currentDrawStyle = prefix;
updateButtonStyles();
const feature = features[0];
const featureId = feature.layer.id.replace(prefix + "-", '');
const allFeatures = draw.getAll().features;
currentFeature = allFeatures.find(f => f.id === featureId);
if (currentFeature) {
const params = paramsMap.get(featureId);
if (params) {
showEditorFunction(params);
}
draw.changeMode('direct_select', { featureId: featureId });
}
}
function DrawArrow(coordinates, polygonId) {
if (!arrowParamsMap.has(polygonId)) {
arrowParamsMap.set(polygonId, structuredClone(DEFAULT_ARROW_PARAMS));
}
const params = arrowParamsMap.get(polygonId);
const arrowGeoJSON = getArrowPolygon(
{
points: coordinates,
splineStep: params.splineStep,
offsetDistance: params.offsetDistance
},
{
calculation: params.calculation,
range: params.range,
minValue: params.minValue
},
{
widthArrow: params.widthArrow,
lengthArrow: params.lengthArrow
}
);
updatePolygonOnMap(ARROW, polygonId, drawOnMap, arrowGeoJSON, params.paintOptions);
}
function DrawFrontline(coordinates, polygonId) {
if (!frontlineParamsMap.has(polygonId)) {
frontlineParamsMap.set(polygonId, structuredClone(DEFAULT_FRONTLINE_PARAMS));
}
const params = frontlineParamsMap.get(polygonId);
const frontlineGeoJSON = getFrontline(
{
points: coordinates,
splineStep: params.splineStep,
offsetDistance: params.offsetDistance,
style: params.style
},
params.protrusion
);
let polygonToDraw = frontlineGeoJSON;
// Only one side
if (frontlineGeoJSON.leftPoly || frontlineGeoJSON.rightPoly) {
polygonToDraw = frontlineGeoJSON.leftPoly || frontlineGeoJSON.rightPoly;
}
// Both sides
if(frontlineGeoJSON.leftPoly && frontlineGeoJSON.rightPoly){
updatePolygonOnMap(FRONTLINE, polygonId, drawOnMap, polygonToDraw, params.paintOptionsLeft);
let additionalPolygonToDraw = frontlineGeoJSON.rightPoly;
drawOnMap(additionalPolygonToDraw, FRONTLINE + "-" + (polygonId + ADDITIONAL_SIDE), params.paintOptionsRight);
return;
}
if (params.style == 1){ // LEFT_SIDE
updatePolygonOnMap(FRONTLINE, polygonId, drawOnMap, polygonToDraw, params.paintOptionsLeft);
}else if(params.style == 2){ // RIGHT_SIDE
updatePolygonOnMap(FRONTLINE, polygonId, drawOnMap, polygonToDraw, params.paintOptionsRight);
}
}
function updatePolygonOnMap(prefix, polygonId, drawFunction, geojson, paintOptions) {
const layerId =  prefix + "-" + polygonId;
if (map.getLayer(layerId)) map.removeLayer(layerId);
if (map.getSource(layerId)) map.removeSource(layerId);
drawFunction(geojson, layerId, paintOptions);
const allFeatures = draw.getAll().features;
currentFeature = allFeatures.find(f => f.id === polygonId);
}
function drawOnMap(frontlineGeoJSON, id, paintOptions = {}) {
if (map.getLayer(id)) map.removeLayer(id);
if (map.getSource(id)) map.removeSource(id);
map.addSource(id, {
type: 'geojson',
data: frontlineGeoJSON
});
map.addLayer({
id: id,
type: 'fill',
source: id,
paint: {
'fill-color': paintOptions['fill-color'],
'fill-opacity': paintOptions['fill-opacity'],
'fill-outline-color': paintOptions['fill-outline-color']
}
});
}
function handleDelete(polygonType)
{
if (!currentFeature) return;
   
const id = currentFeature.id;
draw.delete(id);
//ARROW
if(polygonType == ARROW){
const arrowLayerId = polygonType + "-" + id;
DeleteFromMap(arrowLayerId, id);
}
//Frontline
if(polygonType == FRONTLINE){
const frontlineLayerId = polygonType + "-" + id;
if (frontlineParamsMap.get(id).style == 3){ //BOTH_SIDES
const frontlineLayerIdB = polygonType + "-" + (id + ADDITIONAL_SIDE);
DeleteFromMap(frontlineLayerId, id);
DeleteFromMap(frontlineLayerIdB, id + ADDITIONAL_SIDE);
hideArrowEditor();
hideFrontlineEditor();
return;
}
DeleteFromMap(frontlineLayerId, id);
}
   
hideArrowEditor();
hideFrontlineEditor();
}
function DeleteFromMap(layerId, FeatureId){
if (map.getLayer(layerId)) map.removeLayer(layerId);
if (map.getSource(layerId)) map.removeSource(layerId);
arrowParamsMap.delete(FeatureId);
}
function showArrowEditor(params) {
const popup = document.getElementById('arrow-editor');
popup.style.display = 'block';
popup.style.left = '10px';
popup.style.top = '70px';
document.getElementById('splineStep').value = params.splineStep;
document.getElementById('offsetDistance').value = params.offsetDistance;
document.getElementById('range').value = params.range;
document.getElementById('minValue').value = params.minValue;
document.getElementById('widthArrow').value = params.widthArrow;
document.getElementById('lengthArrow').value = params.lengthArrow;
document.getElementById('arrowFillColor').value = params.paintOptions["fill-color"];
document.getElementById('arrowOutlineColor').value = params.paintOptions["fill-outline-color"];
parseFloat(document.getElementById('arrowOpacity').value = params.paintOptions["fill-opacity"]);
}
function hideArrowEditor() {
const popup = document.getElementById('arrow-editor');
popup.style.display = 'none';
currentFeature = null;
}
function showFrontlineEditor(params) {
const popup = document.getElementById('frontline-editor');
popup.style.display = 'block';
popup.style.left = '10px';
popup.style.top = '70px';
document.getElementById('splineStepFrontline').value = params.splineStep;
document.getElementById('offsetDistanceFrontline').value = params.offsetDistance;
document.getElementById('styleFrontline').value = params.style;
document.getElementById('protrusionLength').value = params.protrusion.length;
document.getElementById('protrusionStartSize').value = params.protrusion.startSize;
document.getElementById('protrusionEndSize').value = params.protrusion.endSize;
document.getElementById('protrusionGap').value = params.protrusion.gap;
document.getElementById('frontlineFillColorLeft').value = params.paintOptionsLeft["fill-color"];
document.getElementById('frontlineOutlineColorLeft').value = params.paintOptionsLeft["fill-outline-color"];
parseFloat(document.getElementById('frontlineOpacityLeft').value = params.paintOptionsLeft["fill-opacity"]);
document.getElementById('frontlineFillColorRight').value = params.paintOptionsRight["fill-color"];
document.getElementById('frontlineOutlineColorRight').value = params.paintOptionsRight["fill-outline-color"];
parseFloat(document.getElementById('frontlineOpacityRight').value = params.paintOptionsRight["fill-opacity"]);
}
function hideFrontlineEditor() {
const popup = document.getElementById('frontline-editor');
popup.style.display = 'none';
currentFeature = null;
}
function updateButtonStyles() {
const buttonsContainer = document.getElementById('drawStyleButtons');
Array.from(buttonsContainer.querySelectorAll('button')).forEach(btn => {
const isSelected = btn.getAttribute('data-style') === currentDrawStyle;
btn.style.backgroundColor = isSelected ? '#333' : '#f0f0f0';
btn.style.color = isSelected ? '#fff' : '#000';
});
}
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('removeArrow').addEventListener('click', () => {
   
handleDelete(ARROW);
});
document.getElementById('removeFrontline').addEventListener('click', () => {
   
handleDelete(FRONTLINE);
});
document.getElementById('applyArrowChanges').addEventListener('click', () => {
if (!currentFeature) return;
const coords = currentFeature.geometry.coordinates;
const id = currentFeature.id;
const splineStep = parseFloat(document.getElementById('splineStep').value);
const offsetDistance = parseFloat(document.getElementById('offsetDistance').value);
const range = parseFloat(document.getElementById('range').value);
const minValue = parseFloat(document.getElementById('minValue').value);
const widthArrow = parseFloat(document.getElementById('widthArrow').value);
const lengthArrow = parseFloat(document.getElementById('lengthArrow').value);
const fillColor = document.getElementById('arrowFillColor').value;
const outlineColor = document.getElementById('arrowOutlineColor').value;
const opacity = parseFloat(document.getElementById('arrowOpacity').value);
const paintOptions = {
'fill-color': fillColor,
'fill-outline-color': outlineColor,
'fill-opacity': opacity
};
arrowParamsMap.set(id, {
splineStep,
offsetDistance,
calculation: ARROW_BODY_STYLE_LINEAR,
range,
minValue,
widthArrow,
lengthArrow,
paintOptions
});
const arrowGeoJSON = getArrowPolygon({
points: coords,
splineStep,
offsetDistance
}, {
calculation: ARROW_BODY_STYLE_LINEAR,
range,
minValue
}, {
widthArrow,
lengthArrow
});
//ARROW
const arrowLayerId = ARROW + "-" + id;
if (map.getLayer(arrowLayerId)) map.removeLayer(arrowLayerId);
if (map.getSource(arrowLayerId)) map.removeSource(arrowLayerId);
drawOnMap(arrowGeoJSON, ARROW + "-" + id, paintOptions);
const allFeatures = draw.getAll().features;
currentFeature = allFeatures.find(f => f.id === id);
});
document.getElementById('applyFrontlineChanges').addEventListener('click', () => {
if (!currentFeature) return;
const coords = currentFeature.geometry.coordinates;
const id = currentFeature.id;
const splineStep = parseFloat(document.getElementById('splineStepFrontline').value);
const offsetDistance = parseFloat(document.getElementById('offsetDistanceFrontline').value);
const style = parseInt(document.getElementById('styleFrontline').value);
const length = parseFloat(document.getElementById('protrusionLength').value);
const startSize = parseFloat(document.getElementById('protrusionStartSize').value);
const endSize = parseFloat(document.getElementById('protrusionEndSize').value);
const gap = parseFloat(document.getElementById('protrusionGap').value);
const fillColorLeft = document.getElementById('frontlineFillColorLeft').value;
const outlineColorLeft = document.getElementById('frontlineOutlineColorLeft').value;
const opacityLeft = parseFloat(document.getElementById('frontlineOpacityLeft').value);
const paintOptionsLeft = {
'fill-color': fillColorLeft,
'fill-outline-color': outlineColorLeft,
'fill-opacity': opacityLeft
};
const fillColorRight = document.getElementById('frontlineFillColorRight').value;
const outlineColorRight = document.getElementById('frontlineOutlineColorRight').value;
const opacityRight = parseFloat(document.getElementById('frontlineOpacityRight').value);
const paintOptionsRight = {
'fill-color': fillColorRight,
'fill-outline-color': outlineColorRight,
'fill-opacity': opacityRight
};
frontlineParamsMap.set(id, {
splineStep,
offsetDistance,
style,
protrusion: {
length,
startSize,
endSize,
gap
},
paintOptionsLeft,
paintOptionsRight
});
const frontlineData = {
points: coords,
splineStep,
offsetDistance,
style
};
const protrusionData = frontlineParamsMap.get(id).protrusion;
const frontlineGeoJSON = getFrontline(frontlineData, protrusionData);
let polygonToDraw = frontlineGeoJSON;
if (frontlineGeoJSON.leftPoly || frontlineGeoJSON.rightPoly) {
polygonToDraw = frontlineGeoJSON.leftPoly || frontlineGeoJSON.rightPoly;
}
if(frontlineGeoJSON.leftPoly && frontlineGeoJSON.rightPoly){
let polygonToDrawLeft = frontlineGeoJSON.leftPoly;
let polygonToDrawRight = frontlineGeoJSON.rightPoly;
const frontlineLayerId = FRONTLINE + "-" + id;
if (map.getLayer(frontlineLayerId)) map.removeLayer(frontlineLayerId);
if (map.getSource(frontlineLayerId)) map.removeSource(frontlineLayerId);
drawOnMap(polygonToDrawLeft, FRONTLINE + "-" + id + ADDITIONAL_SIDE, paintOptionsLeft);
drawOnMap(polygonToDrawRight, FRONTLINE + "-" + (id), paintOptionsRight);
const allFeatures = draw.getAll().features;
currentFeature = allFeatures.find(f => f.id === id);
return;
}
const frontlineLayerId = FRONTLINE + "-" + id;
if (map.getLayer(frontlineLayerId + ADDITIONAL_SIDE)) map.removeLayer(frontlineLayerId + ADDITIONAL_SIDE);
if (map.getSource(frontlineLayerId + ADDITIONAL_SIDE)) map.removeSource(frontlineLayerId + ADDITIONAL_SIDE);
if (map.getLayer(frontlineLayerId)) map.removeLayer(frontlineLayerId);
if (map.getSource(frontlineLayerId)) map.removeSource(frontlineLayerId);
let paintOptions;
if(frontlineData.style == LEFT_SIDE){
paintOptions = paintOptionsLeft;
}else if(frontlineData.style == RIGHT_SIDE)
{
paintOptions = paintOptionsRight;
}
drawOnMap(polygonToDraw, FRONTLINE + "-" + id, paintOptions);
const allFeatures = draw.getAll().features;
currentFeature = allFeatures.find(f => f.id === id);
});
const buttonsContainer = document.getElementById('drawStyleButtons');
buttonsContainer.addEventListener('click', (event) => {
if (event.target.tagName !== 'BUTTON') return;
Array.from(buttonsContainer.querySelectorAll('button')).forEach(btn => btn.classList.remove('active'));
event.target.classList.add('active');
currentDrawStyle = event.target.getAttribute('data-style');
updateButtonStyles();
});
updateButtonStyles();
});