Added collision avoidance and marker methods

This commit is contained in:
Ray 2023-10-02 23:42:08 +01:00
parent 00c38a5e00
commit f935d337d6
3 changed files with 412 additions and 158 deletions

View File

View File

@ -36,16 +36,21 @@ class BBTimeline {
BorderColour: "#3A5D9C",
BorderWidth: 2,
BackColour: "#D4DEEF",
Width: 10
Width: 10,
ForeColour: "#3A5D9C",
Font: "9pt Arial",
LabelCollisionMargin: 4
};
a.HighlightLine = {
Colour: "#A6A6A6",
Width: 1,
};
a.Events = [];
a.StartDate = new Date();
a.ClientRectangle = a.getClientRectangle();
a.StartDate = a.DateToString(new Date(), "yyyy-MM-dd");
a.ShowDate = a.StartDate;
a.GraphRectangle = a.calcGraphArea();
a.Enabled = false;
a.Debug = false;
a.ctx = a.Container.getContext("2d");
a.ctx.canvas.width = a.Size.Width;
@ -54,32 +59,50 @@ class BBTimeline {
a.initialiseComponents();
}
Load(startDate) {
AddEvent(date, label, options) {
const a = this;
a.StartDate = ((typeof(startDate) == "undefined") ? new Date() : startDate);
const _options = Object.assign(a.GetEventItem(), options);
a.invalidate(true);
let event = a.FindEvent(date);
if (event == null) {
a.Events.push(a.GetEvent(date));
a.Enabled = true;
event = a.FindEvent(date);
}
Clear(all) {
if (label != null) {
event.Label = label;
}
event.Events.push(_options);
}
Clear() {
const a = this;
if (all) {
a.ctx.clearRect(0, 0, a.ctx.canvas.width, a.ctx.canvas.height);
a.Events = [];
a.StartDate = a.DateToString(new Date(), "yyyy-MM-dd");
a.ShowDate = a.StartDate;
a.Enabled = false;
} else {
const rect = a.getClientCoords();
a.Events = [];
}
a.ctx.clearRect((rect.X1 + a.Axis.LineWidth), rect.Y1, (rect.X2 - rect.X1 - a.Axis.LineWidth), (rect.Y2 - rect.Y1 - a.Axis.LineWidth));
DeleteMarker(date)
{
const a = this;
for (let i=0; i<a.Events.length; i++) {
if (a.Events[i].Date != date) {
continue;
}
a.Events.splice(i, 1);
}
}
FindDate(date) {
FindDatePosition(date) {
const a = this;
const points = a.getXAxis();
@ -92,11 +115,46 @@ class BBTimeline {
return null;
}
FindEventByCoords(x, y) {
FindVisibleEvents() {
const a = this;
let result = [];
const availableX = a.getXAxis();
availableX.forEach(function (e) {
const event = a.FindEvent(e.Date);
if (event == null) {
return;
}
// Set offsetX on current view
event.Position.X = e.X;
result.push(event);
});
return result;
}
FindEvent(date) {
const a = this;
for (let i=0; i<a.Events.length; i++) {
const e = a.Events[i].Rectangle;
if (a.Events[i].Date == date) {
return a.Events[i];
}
}
return null;
}
FindEventsByCoords(x, y) {
const a = this;
for (let i=0; i<a.Events.length; i++) {
const e = a.Events[i].HitBox;
if (a.Events[i].HitBox == null) {
continue;
}
if ((x >= e.X1) && (x <= e.X2) && (y >= e.Y1) && (y <= e.Y2)){
return a.Events[i];
@ -106,77 +164,265 @@ class BBTimeline {
return null;
}
AddEvent(date, title, description, link) {
Load(startDate) {
const a = this;
a.Events.push({
Date: date,
Title: title,
Description: description,
Link: link
});
a.StartDate = startDate;
a.invalidate(false);
a.Show(startDate);
}
Show(date) {
const a = this;
if (a.stringToDate(date) < a.stringToDate(a.StartDate)) {
date = a.StartDate;
}
a.ShowDate = date;
a.Enabled = true;
a.Invalidate(true, true);
}
ShowNext() {
const a = this;
let date = a.stringToDate(a.ShowDate);
date.setMonth(date.getMonth() + 1);
a.Show(a.DateToString(date, "yyyy-MM-dd"));
}
ShowPrevious() {
const a = this;
let date = a.stringToDate(a.ShowDate);
date.setMonth(date.getMonth() - 1);
a.Show(a.DateToString(date, "yyyy-MM-dd"));
}
UpdateLabel(date, label) {
const a = this;
let event = a.FindEvent(date);
if (event == null) {
return;
}
event.Label = label;
a.Invalidate(false, true);
}
UpdateMarker(date, borderColour, backColour) {
const a = this;
let event = a.FindEvent(date);
if (event == null) {
return;
}
event.BorderColour = borderColour;
event.BackColour = backColour;
a.Invalidate(false, true);
}
initialiseComponents() {
const a = this;
const coords = a.getClientCoords();
// Vertical highlight line
// a.ctx.canvas.addEventListener('mousemove', function (e) {
// if (!a.Enabled) {
// return;
// }
// a.invalidate(false);
// if ((e.offsetX > (coords.X1 + a.Axis.LineWidth)) && (e.offsetX < coords.X2) && (e.offsetY >= coords.Y1) && (e.offsetY < (coords.Y2 - a.Axis.LineWidth))){
// a.drawVerticalLine(e.offsetX);
// }
// });
a.ctx.canvas.addEventListener('mousedown', function (e) {
if (!a.Enabled) {
return;
}
var event = a.FindEventByCoords(e.offsetX, e.offsetY);
var event = a.FindEventsByCoords(e.offsetX, e.offsetY);
if (event == null) {
return;
}
console.log(event);
if (a.Debug) console.log(event);
// console.log(e);
a.OnEventClick(event);
});
}
invalidate(all) {
Invalidate(redrawAxis, redrawMarkers) {
const a = this;
a.Clear(all);
if (redrawAxis) {
a.ctx.clearRect(0, 0, a.ctx.canvas.width, a.ctx.canvas.height);
if (all) {
a.drawAxis();
a.drawXAxis();
a.drawXAxisLabels();
} else {
}
if (redrawMarkers) {
a.clearChart();
const visibleEvents = a.FindVisibleEvents();
const coords = a.getClientCoords();
coords.Y1 += a.Marker.Width;
for (let i=0; i<a.Events.length; i++) {
const event = a.FindDate(a.Events[i].Date);
if (a.Debug) console.log(visibleEvents);
a.drawVerticalLine(event.X, coords.Y1);
visibleEvents.forEach(function (e, i) {
// Calculate Y position
let posY = a.calcMarkerPosition(e.Position.X, coords.Y1);
const rect = a.drawMarker(event.X, coords.Y1);
a.Events[i].Rectangle = rect;
}
a.drawVerticalLine(e.Position.X, posY);
const markerCoords = a.drawMarker(e.Position.X, posY, e.BorderColour, e.BackColour);
const labelSize = a.drawText((markerCoords.X2 + a.GraphRectangle.Margin), markerCoords.Y1, e.Label, a.Marker.Font, a.Marker.ForeColour, "left");
console.log(a.Events);
e.Position = { X: e.Position.X, Y: posY };
e.HitBox = {
X1: markerCoords.X1,
Y1: markerCoords.Y1,
X2: (markerCoords.X2 + a.GraphRectangle.Margin + labelSize.Width),
Y2: markerCoords.Y2
};
if (a.Debug) a.drawRectangle(e.HitBox);
if (a.Debug) console.log(e);
});
}
}
GetEvent(date) {
const a = this;
return {
Date: date,
Label: "",
Position: { X: 0, Y: 0 },
Events: [],
HitBox: null,
BorderColour: a.Marker.BorderColour,
BackColour: a.Marker.BackColour
};
}
GetEventItem() {
return {
Title: "",
Description: "",
Link: "",
Tag: null
};
}
DateToString(date, pattern) {
let result = pattern;
result = result.replace("fffffff", date.getMilliseconds().toString().padStart(7, '0'));
result = result.replace("ffffff", date.getMilliseconds().toString().padStart(6, '0'));
result = result.replace("fffff", date.getMilliseconds().toString().padStart(5, '0'));
result = result.replace("yyyy", date.getFullYear().toString().padStart(4, '0'));
result = result.replace("MMMM", "{1}");
result = result.replace("dddd", "{2}");
result = result.replace("ffff", date.getMilliseconds().toString().padStart(4, '0'));
result = result.replace("yyy", date.getFullYear().toString().padStart(3, '0'));
result = result.replace("MMM", "{3}");
result = result.replace("ddd", "{4}");
result = result.replace("fff", date.getMilliseconds().toString().padStart(3, '0'));
result = result.replace("zzz", "");
result = result.replace("yy", date.getFullYear().toString().slice(-2));
result = result.replace("MM", (date.getMonth() + 1).toString().padStart(2, '0'));
result = result.replace("dd", date.getDate().toString().padStart(2, '0'));
result = result.replace("HH", date.getHours().toString().padStart(2, '0'));
result = result.replace("hh", (date.getHours() > 12 ? (date.getHours() - 12) : date.getHours()).toString().padStart(2, '0'));
result = result.replace("mm", date.getMinutes().toString().padStart(2, '0'));
result = result.replace("ss", date.getSeconds().toString().padStart(2, '0'));
result = result.replace("ff", date.getMilliseconds().toString().padStart(2, '0'));
result = result.replace("tt", "{5}");
result = result.replace("zz", "");
result = result.replace("y", date.getFullYear().toString());
result = result.replace("M", (date.getMonth() + 1).toString());
result = result.replace("d", date.getDate().toString());
result = result.replace("H", date.getHours().toString());
result = result.replace("h", (date.getHours() > 12 ? (date.getHours() - 12) : date.getHours()).toString());
result = result.replace("m", date.getMinutes().toString());
result = result.replace("s", date.getSeconds().toString());
result = result.replace("z", "");
result = result.replace("t", "{6}");
result = result.replace("Z", "");
result = result.replace("{1}", date.toLocaleString('default', { month: 'long' }));
result = result.replace("{2}", date.toLocaleString('default', { weekday: 'long' }));
result = result.replace("{3}", date.toLocaleString('default', { month: 'short' }));
result = result.replace("{4}", date.toLocaleString('default', { weekday: 'short' }));
result = result.replace("{5}", (date.getHours() >= 12 ? "PM" : "AM"));
result = result.replace("{6}", (date.getHours() >= 12 ? "P" : "A"));
return result;
}
OnEventClick(event) {
}
calcGraphArea() {
const a = this;
let result = {
X: a.Padding.Left,
Y: a.Padding.Top,
Width: (a.Size.Width - a.Padding.Right),
Height: (a.Size.Height - a.Padding.Bottom),
Margin: (a.Marker.BorderWidth * 2)
};
result.StepHeight = a.Marker.Width + result.Margin;
result.NoStep = Math.floor(result.Height / result.StepHeight);
return result;
}
calcMarkerPosition(x, y) {
const a = this;
// Calculate Y position
let hasMoved = false;
let posY = y;
for (let i=0; i<a.GraphRectangle.NoStep; i++)
{
posY = y + (a.GraphRectangle.StepHeight * i);
var clippedEvent = a.FindEventsByCoords(x, posY);
if (clippedEvent == null) {
hasMoved = true;
break;
}
}
if (!hasMoved) {
posY = y;
}
return posY;
}
clearChart() {
const a = this;
const rect = a.getClientCoords();
a.ctx.clearRect((rect.X1 + a.Axis.LineWidth), rect.Y1, (rect.X2 - rect.X1 - a.Axis.LineWidth), (rect.Y2 - rect.Y1 - a.Axis.LineWidth));
// Clear marker positions
const visibleEvents = a.FindVisibleEvents();
visibleEvents.forEach(function (e, i) {
e.Position = { X: 0, Y: 0 };
e.HitBox = null;
});
}
drawAxis() {
const a = this;
const coords = a.getClientCoords();
@ -191,6 +437,8 @@ class BBTimeline {
a.ctx.lineWidth = a.Axis.LineWidth;
a.ctx.strokeStyle = a.Axis.LineColour1;
a.ctx.stroke();
if (a.Debug) a.drawRectangle(coords);
}
drawXAxis() {
@ -258,17 +506,17 @@ class BBTimeline {
// return;
// }
const labelSize = a.drawText(e.X, (y + a.Axis.X.DayLineHeight), a.dateToString(date, "dd"), "center");
const labelSize = a.drawText(e.X, (y + a.Axis.X.DayLineHeight), a.DateToString(date, "dd"), a.Axis.Font, a.Axis.LabelColour, "center");
const label2Spacing = 6;
// Write month on first of the month
if (writeLabel) {
a.drawText(e.X, (y + a.Axis.X.DayLineHeight + labelSize.Height + label2Spacing), a.dateToString(date, "MMMM yyyy"), "left");
a.drawText(e.X, (y + a.Axis.X.DayLineHeight + labelSize.Height + label2Spacing), a.DateToString(date, "MMMM yyyy"), a.Axis.Font, a.Axis.LabelColour, "left");
}
});
}
drawMarker(x, y) {
drawMarker(x, y, borderColour, backColour) {
const a = this;
const coords = a.getClientCoords();
if (coords == null) {
@ -279,39 +527,22 @@ class BBTimeline {
a.ctx.beginPath();
a.ctx.arc(x, y, width, 0, 2 * Math.PI, false);
a.ctx.fillStyle = a.Marker.BackColour;
a.ctx.fillStyle = backColour;
a.ctx.fill();
a.ctx.lineWidth = a.Marker.BorderWidth;
a.ctx.strokeStyle = a.Marker.BorderColour;
a.ctx.strokeStyle = borderColour;
a.ctx.stroke();
const offset = a.half(a.Marker.Width);
const result = {
X1: x - (offset + a.Marker.BorderWidth),
Y1: y - (offset + a.Marker.BorderWidth),
X2: x + (offset + a.Marker.BorderWidth),
Y2: y + (offset + a.Marker.BorderWidth)
};
// a.ctx.beginPath();
// a.ctx.rect(result.X1, result.Y1, (result.X2 - result.X1), (result.Y2 - result.Y1));
// a.ctx.fillStyle = 'yellow';
// a.ctx.fill();
// a.ctx.lineWidth = 1;
// a.ctx.strokeStyle = 'black';
// a.ctx.stroke();
return result;
return a.measureMarker(x, y);
}
drawText(x, y, label, align) {
drawText(x, y, label, font, foreColour, align) {
const a = this;
a.ctx.font = a.Axis.Font;
a.ctx.fillStyle = a.Axis.LabelColour;
a.ctx.font = font;
a.ctx.fillStyle = foreColour;
const size = a.measureText(label);
const size = a.measureText(font, label);
switch (align) {
case "center":
@ -331,6 +562,18 @@ class BBTimeline {
return size;
}
drawRectangle(coords) {
const a = this;
a.ctx.beginPath();
a.ctx.rect(coords.X1, coords.Y1, (coords.X2 - coords.X1), (coords.Y2 - coords.Y1));
//a.ctx.fillStyle = 'yellow';
//a.ctx.fill();
a.ctx.lineWidth = 1;
a.ctx.strokeStyle = 'red';
a.ctx.stroke();
}
drawVerticalLine(x, y) {
const a = this;
const coords = a.getClientCoords();
@ -353,26 +596,15 @@ class BBTimeline {
getClientCoords() {
const a = this;
if (a.ClientRectangle == null) {
if (a.GraphRectangle == null) {
return null;
}
return {
X1: a.ClientRectangle.X,
Y1: a.ClientRectangle.Y,
X2: (a.ClientRectangle.Width - a.ClientRectangle.X),
Y2: (a.ClientRectangle.Height - a.ClientRectangle.Y)
};
}
getClientRectangle() {
const a = this;
return {
X: a.Padding.Left,
Y: a.Padding.Top,
Width: (a.Size.Width - a.Padding.Right),
Height: (a.Size.Height - a.Padding.Bottom)
X1: a.GraphRectangle.X,
Y1: a.GraphRectangle.Y,
X2: (a.GraphRectangle.Width - a.GraphRectangle.X),
Y2: (a.GraphRectangle.Height - a.GraphRectangle.Y)
};
}
@ -385,7 +617,7 @@ class BBTimeline {
let result = [];
let x = coords.X1;
let date = a.stringToDate(a.dateToString(a.StartDate, "yyyy-MM-dd"));
let date = a.stringToDate(a.ShowDate);
// Rollback one day
date.setDate(date.getDate() - 1);
@ -396,7 +628,7 @@ class BBTimeline {
}
result.push({
Date: a.dateToString(date, "yyyy-MM-dd"),
Date: a.DateToString(date, "yyyy-MM-dd"),
X: x
});
@ -411,9 +643,24 @@ class BBTimeline {
return (value / 2);
}
measureText(value) {
measureMarker(x, y) {
const a = this;
const offset = a.half(a.Marker.Width);
const result = {
X1: x - (offset + a.Marker.BorderWidth),
Y1: y - (offset + a.Marker.BorderWidth),
X2: x + (offset + a.Marker.BorderWidth),
Y2: y + (offset + a.Marker.BorderWidth)
};
return result;
}
measureText(font, value) {
const a = this;
a.ctx.font = font;
const size = a.ctx.measureText(value);
return {
@ -424,52 +671,6 @@ class BBTimeline {
};
}
dateToString(date, pattern) {
let result = pattern;
result = result.replace("fffffff", date.getMilliseconds().toString().padStart(7, '0'));
result = result.replace("ffffff", date.getMilliseconds().toString().padStart(6, '0'));
result = result.replace("fffff", date.getMilliseconds().toString().padStart(5, '0'));
result = result.replace("yyyy", date.getFullYear().toString().padStart(4, '0'));
result = result.replace("MMMM", "{1}");
result = result.replace("dddd", "{2}");
result = result.replace("ffff", date.getMilliseconds().toString().padStart(4, '0'));
result = result.replace("yyy", date.getFullYear().toString().padStart(3, '0'));
result = result.replace("MMM", "{3}");
result = result.replace("ddd", "{4}");
result = result.replace("fff", date.getMilliseconds().toString().padStart(3, '0'));
result = result.replace("zzz", "");
result = result.replace("yy", date.getFullYear().toString().slice(-2));
result = result.replace("MM", (date.getMonth() + 1).toString().padStart(2, '0'));
result = result.replace("dd", date.getDate().toString().padStart(2, '0'));
result = result.replace("HH", date.getHours().toString().padStart(2, '0'));
result = result.replace("hh", (date.getHours() > 12 ? (date.getHours() - 12) : date.getHours()).toString().padStart(2, '0'));
result = result.replace("mm", date.getMinutes().toString().padStart(2, '0'));
result = result.replace("ss", date.getSeconds().toString().padStart(2, '0'));
result = result.replace("ff", date.getMilliseconds().toString().padStart(2, '0'));
result = result.replace("tt", "{5}");
result = result.replace("zz", "");
result = result.replace("y", date.getFullYear().toString());
result = result.replace("M", (date.getMonth() + 1).toString());
result = result.replace("d", date.getDate().toString());
result = result.replace("H", date.getHours().toString());
result = result.replace("h", (date.getHours() > 12 ? (date.getHours() - 12) : date.getHours()).toString());
result = result.replace("m", date.getMinutes().toString());
result = result.replace("s", date.getSeconds().toString());
result = result.replace("z", "");
result = result.replace("t", "{6}");
result = result.replace("Z", "");
result = result.replace("{1}", date.toLocaleString('default', { month: 'long' }));
result = result.replace("{2}", date.toLocaleString('default', { weekday: 'long' }));
result = result.replace("{3}", date.toLocaleString('default', { month: 'short' }));
result = result.replace("{4}", date.toLocaleString('default', { weekday: 'short' }));
result = result.replace("{5}", (date.getHours() >= 12 ? "PM" : "AM"));
result = result.replace("{6}", (date.getHours() >= 12 ? "P" : "A"));
return result;
}
stringToDate(value) {
return new Date(Date.parse(value));
}

View File

@ -10,9 +10,7 @@
<!-- <script src="http://cdn.hiimray.co.uk/8206c600-707c-469e-8d49-a76ae35782af/bootstrap/5.3.0/dist/js/bootstrap.bundle.min.js"></script> -->
<!-- <link href="http://cdn.hiimray.co.uk/8206c600-707c-469e-8d49-a76ae35782af/bootstrap/5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" /> -->
<link href="bbtimeline.css" rel="stylesheet" />
<script src="bbtimeline.js"></script>
<!-- <link href="bbtimeline.min.css" rel="stylesheet" /> -->
<!-- <script src="bbtimeline.min.js"></script> -->
<title></title>
@ -23,9 +21,18 @@
<p>
<button onclick="Clear()">Clear</button>
<button onclick="LoadToday()">Load Today</button>
<button onclick="LoadNext()">Load Next Month</button>
<button onclick="LoadDemo()">Load Demo Events</button>
<button onclick="ToggleDebug()">Toggle Debug</button>
</p>
<p>
<button onclick="LoadToday()">Show Today</button>
<button onclick="LoadPrevious()">Show Previous Month</button>
<button onclick="LoadNext()">Show Next Month</button>
</p>
<p>
<button onclick="UpdateLabel()">Update Label</button>
<button onclick="UpdateMarker()">Update Marker</button>
<button onclick="DeleteMarker()">Delete Marker</button>
</p>
<style>
@ -49,11 +56,13 @@ canvas {
<script>
var timeline1 = new BBTimeline("myCanvas");
timeline1.OnEventClick = function(e) {
// console.log(e);
}
LoadDemo();
LoadToday();
timeline1.Load();
timeline1.AddEvent("2023-10-05", "title", "description", "link");
timeline1.AddEvent("2023-10-06", "title", "description", "link");
timeline1.AddEvent("2023-10-16", "title", "description", "link");
@ -63,17 +72,61 @@ function Clear()
timeline1.Clear(true);
}
function LoadDemo()
{
timeline1.AddEvent("2023-10-05", "hello", { Title: Math.random(), Description: Math.random() });
timeline1.AddEvent("2023-10-06", "hello", { Title: Math.random(), Description: Math.random() });
timeline1.AddEvent("2023-10-06", null, { Title: Math.random(), Description: Math.random() });
timeline1.AddEvent("2023-10-16", "hello", { Title: Math.random(), Description: Math.random() });
timeline1.AddEvent("2023-10-06", "hello 2", { Title: Math.random(), Description: Math.random() });
timeline1.AddEvent("2023-10-06", null, { Title: Math.random(), Description: Math.random() });
timeline1.AddEvent("2023-10-20", "hello hello hello", { Title: Math.random(), Description: Math.random() });
timeline1.AddEvent("2023-10-21", "hello", { Title: Math.random(), Description: Math.random() });
timeline1.AddEvent("2023-10-23", "hello", { Title: Math.random(), Description: Math.random() });
timeline1.AddEvent("2023-10-26", "hello", { Title: Math.random(), Description: Math.random() });
LoadToday();
timeline1.Invalidate(false, true);
}
function ToggleDebug()
{
timeline1.Debug = !timeline1.Debug;
timeline1.Invalidate(true, true);
}
function LoadToday()
{
timeline1.Load(new Date());
const date = timeline1.DateToString(new Date(), "yyyy-MM-dd");
timeline1.Load(date);
}
function LoadPrevious()
{
timeline1.ShowPrevious();
}
function LoadNext()
{
let date = timeline1.StartDate;
date.setMonth(date.getMonth() + 1);
timeline1.ShowNext();
}
timeline1.Load(date);
function UpdateLabel()
{
timeline1.UpdateLabel("2023-10-21", "hello world");
}
function UpdateMarker()
{
timeline1.UpdateMarker("2023-10-21", "#E68422", "#FAE7D3");
}
function DeleteMarker()
{
timeline1.DeleteMarker("2023-10-06");
timeline1.Invalidate(false, true);
}
</script>