Skip to content
148 changes: 98 additions & 50 deletions src/BinMousePosition.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,35 @@ import {
identityTransform,
} from "ol/proj";
import TileLayer from "ol/layer/Tile";
import { useUiSettingsStore } from "@/app/stores/uiSettingsStore";

function getOsdSettings() {
try {
return useUiSettingsStore();
} catch {
return null;
}
}

function overlayStyle(position) {
const location =
position === "bottom-left"
? "left: 12px; bottom: 12px;"
: "right: 12px; top: 12px;";
return [
"display: block",
"position: absolute",
location,
"max-width: min(48rem, calc(100vw - 3rem))",
"padding: 14px 16px",
"background: rgba(0, 0, 0, 0.35)",
"border: 1px solid black",
"border-radius: 12px",
"color: white",
"text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000",
"pointer-events: none",
].join("; ");
}

export default class BinMousePosition extends MousePosition {
constructor(opt_options) {
Expand Down Expand Up @@ -133,6 +162,24 @@ export default class BinMousePosition extends MousePosition {
this.getDimensionHolderForDescriptor(guidanceDescriptor);
const bpResolution = guidanceDescriptor.bpResolution;
const pixelResolution = guidanceDescriptor.pixelResolution;
const osdSettings = getOsdSettings();
if (osdSettings && !osdSettings.osdOverlayVisible) {
html = "";
if (!this.renderedHTML_ || html !== this.renderedHTML_) {
this.element.innerHTML = html;
this.renderedHTML_ = html;
}
return;
}
const fields = osdSettings?.osdOverlayFields ?? {};
const fieldOrder = osdSettings?.osdOverlayFieldOrder ?? [];
const osdLines = {};
const fieldEnabled = (field) => fields[field] !== false;
const appendLine = (field, text) => {
if (fieldEnabled(field)) {
osdLines[field] = text;
}
};
const fixed_coordinates = mapCoordinate.map((c) =>
Math.ceil(c / pixelResolution)
);
Expand All @@ -145,36 +192,36 @@ export default class BinMousePosition extends MousePosition {
bpResolution
);

html =
'<div style="display: block; padding: 20px; background: rgba(0, 0, 0, 0.35); border: 1px solid black; border-radius: 15px; color: white; text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000;">';
html +=
"Global projection coordinate: " + coordinate.map(Math.floor);
html = html + "<";
html = html + "br/>";
this.element.style.position = "absolute";
this.element.style.inset = "0";
this.element.style.pointerEvents = "none";
html = `<div style="${overlayStyle(osdSettings?.osdOverlayPosition)}">`;
appendLine(
"global",
"Global projection coordinate: " + coordinate.map(Math.floor)
);

// html +=
// "Center coordinate: " + map.getView().getCenter().map(Math.floor);
// html = html + "<";
// html = html + "br/>";

if (fixed_coordinates) {
html = html + "Bin resolution: 1:" + bpResolution;
html = html + "<";
html = html + "br/>";
appendLine("resolution", "Bin resolution: 1:" + bpResolution);
if (
Number.isFinite(hoveredBpResolution) &&
hoveredBpResolution !== bpResolution
) {
html =
html + "Hovered tile resolution: 1:" + hoveredBpResolution;
html = html + "<";
html = html + "br/>";
appendLine(
"resolution",
"Hovered tile resolution: 1:" + hoveredBpResolution
);
}
if (guidanceDescriptor.sourceName) {
html =
html + "Guidance source: " + guidanceDescriptor.sourceName;
html = html + "<";
html = html + "br/>";
appendLine(
"source",
"Guidance source: " + guidanceDescriptor.sourceName
);
}
if (this.layersManager?.getVisibleSourceResolutionDescriptors) {
const visible =
Expand All @@ -188,35 +235,33 @@ export default class BinMousePosition extends MousePosition {
: null,
].filter(Boolean);
if (details.length > 0) {
html =
html + "Visible source resolutions: " + details.join("; ");
html = html + "<";
html = html + "br/>";
appendLine(
"visibleResolutions",
"Visible source resolutions: " + details.join("; ")
);
}
}
html =
html +
appendLine(
"pixels",
"Position: px1=" +
int_coordinates_px[0] +
" px2=" +
int_coordinates_px[1];
int_coordinates_px[1]
);
}

if (dimensionHolder) {
const int_coordinates_bins = dimensionHolder.pixelsToBins(
int_coordinates_px,
bpResolution
);
html = html + "<";
html = html + "br/>";
html =
html +
appendLine(
"bins",
"Position: bin1=" +
int_coordinates_bins[0] +
" bin2=" +
int_coordinates_bins[1];
html = html + "<";
html = html + "br/>";
int_coordinates_bins[1]
);
const bp1 = dimensionHolder.getStartBpOfPx(
int_coordinates_px[0],
bpResolution
Expand All @@ -233,10 +278,8 @@ export default class BinMousePosition extends MousePosition {
int_coordinates_px[1],
bpResolution
);
html = html + "Position: bp1=" + bp1 + " bp2=" + bp2;
html = html + "<";
html = html + "br/>";
html = html + "Contigs: ctg1=" + ctg1 + " ctg2=" + ctg2;
appendLine("basePairs", "Position: bp1=" + bp1 + " bp2=" + bp2);
appendLine("contigs", "Contigs: ctg1=" + ctg1 + " ctg2=" + ctg2);
if (dimensionHolder.getContigLocusByPx) {
const locus1 = dimensionHolder.getContigLocusByPx(
int_coordinates_px[0],
Expand All @@ -246,39 +289,44 @@ export default class BinMousePosition extends MousePosition {
int_coordinates_px[1],
bpResolution
);
html = html + "<";
html = html + "br/>";
html =
html +
appendLine(
"inContig",
"In-contig bp: ctg1=+" +
locus1.inContigBp +
" ctg2=+" +
locus2.inContigBp;
locus2.inContigBp
);
}
if (this.scaffold_holder?.getScaffoldLocusByBp) {
const scaffold1 =
this.scaffold_holder.getScaffoldLocusByBp(bp1);
const scaffold2 =
this.scaffold_holder.getScaffoldLocusByBp(bp2);
html = html + "<";
html = html + "br/>";
html =
html +
appendLine(
"scaffolds",
"Scaffolds: scf1=" +
(scaffold1 ? scaffold1.scaffoldName : "unscaffolded") +
" scf2=" +
(scaffold2 ? scaffold2.scaffoldName : "unscaffolded");
html = html + "<";
html = html + "br/>";
html =
html +
(scaffold2 ? scaffold2.scaffoldName : "unscaffolded")
);
appendLine(
"inScaffold",
"In-scaffold bp: scf1=" +
(scaffold1 ? "+" + scaffold1.inScaffoldBp : "n/a") +
" scf2=" +
(scaffold2 ? "+" + scaffold2.inScaffoldBp : "n/a");
(scaffold2 ? "+" + scaffold2.inScaffoldBp : "n/a")
);
}
}

const orderedFields = [
...fieldOrder,
...Object.keys(osdLines).filter((field) => !fieldOrder.includes(field)),
];
const orderedLines = orderedFields
.map((field) => osdLines[field])
.filter(Boolean);
html += orderedLines.join("<br/>");
html += "</div>";
} catch (error) {
console.warn("Unable to update map mouse position overlay", error);
Expand Down
80 changes: 75 additions & 5 deletions src/app/core/controls/RulerControl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { transform } from "ol/proj";
import { storeToRefs } from "pinia";
import { useStyleStore } from "@/app/stores/styleStore";
import { useVisualizationOptionsStore } from "@/app/stores/visualizationOptionsStore";
import { useUiSettingsStore } from "@/app/stores/uiSettingsStore";
import { Ref } from "vue";
import Colormap from "../visualization/colormap/Colormap";
import { ColorTranslator } from "colortranslator";
Expand All @@ -49,6 +50,8 @@ interface RulerTick {
mapPx: number;
bp: number;
label: string;
contigLabel?: string;
scaffoldLabel?: string;
major: boolean;
boundary?: "start" | "end";
}
Expand All @@ -62,6 +65,7 @@ class RulerControl extends Control {

protected readonly mapBackgroundColor: Ref<ColorTranslator>;
protected readonly colormap: Ref<Colormap>;
protected readonly rulerCoordinateMode: Ref<"global" | "contig" | "scaffold">;

public readonly canvasSize: number[];
public readonly direction: "vertical" | "horizontal";
Expand Down Expand Up @@ -137,9 +141,12 @@ class RulerControl extends Control {
const { colormap } = storeToRefs(visualizationOptionsStore);
const stylesStore = useStyleStore();
const { mapBackgroundColor } = storeToRefs(stylesStore);
const uiSettingsStore = useUiSettingsStore();
const { rulerCoordinateMode } = storeToRefs(uiSettingsStore);

this.colormap = colormap;
this.mapBackgroundColor = mapBackgroundColor as Ref<ColorTranslator>;
this.rulerCoordinateMode = rulerCoordinateMode;

this.canvasSize = canvasSize;
this.canvas.addEventListener("mousemove", (event) =>
Expand Down Expand Up @@ -544,20 +551,30 @@ class RulerControl extends Control {
options.resolutionDescriptor.bpResolution
)
);
const maxBp = Math.max(...bpValues, 0);
const maxBp = Math.max(
...bpValues.map((bp) => this.coordinateValueForMode(bp)),
0
);
const labelUnit = this.absoluteLabelUnit(maxBp);
let currentAnchor = this.roundDownToUnit(bpValues[0] ?? 0, labelUnit);
let currentAnchor = this.roundDownToUnit(
this.coordinateValueForMode(bpValues[0] ?? 0),
labelUnit
);
return screens.map((screen, index) => {
const bp = bpValues[index] ?? 0;
const mapPx = mapPxValues[index] ?? 0;
const anchor = this.roundDownToUnit(bp, labelUnit);
const coordinateValue = this.coordinateValueForMode(bp);
const anchor = this.roundDownToUnit(coordinateValue, labelUnit);
const boundary =
index === 0 ? "start" : index === screens.length - 1 ? "end" : undefined;
const major = boundary !== undefined || anchor !== currentAnchor;
if (major) {
currentAnchor = anchor;
}
const delta = Math.max(0, Math.round(bp - currentAnchor));
const delta = Math.max(0, Math.round(coordinateValue - currentAnchor));
const contig = this.contigDimensionHolder.getContigLocusByBp(bp);
const scaffold = this.mapManager.scaffoldHolder.getScaffoldLocusByBp(bp);
const prefix = this.coordinatePrefixForMode();
return {
screen,
mapPx,
Expand All @@ -566,12 +583,39 @@ class RulerControl extends Control {
boundary,
label:
major || delta <= 0
? this.formatBpLabel(anchor, 0)
? `${prefix}${this.formatBpLabel(anchor, 0)}`
: `+${this.formatBpLabel(delta, 0)}`,
contigLabel: major && this.rulerCoordinateMode.value === "global"
? `ctg +${this.formatBpLabel(contig.inContigBp, 0)}`
: undefined,
scaffoldLabel:
major && scaffold && this.rulerCoordinateMode.value === "global"
? `scf +${this.formatBpLabel(scaffold.inScaffoldBp, 0)}`
: undefined,
};
});
}

private coordinateValueForMode(bp: number): number {
if (this.rulerCoordinateMode.value === "contig") {
return this.contigDimensionHolder.getContigLocusByBp(bp).inContigBp;
}
if (this.rulerCoordinateMode.value === "scaffold") {
return this.mapManager.scaffoldHolder.getScaffoldLocusByBp(bp)?.inScaffoldBp ?? bp;
}
return bp;
}

private coordinatePrefixForMode(): string {
if (this.rulerCoordinateMode.value === "contig") {
return "ctg ";
}
if (this.rulerCoordinateMode.value === "scaffold") {
return "scf ";
}
return "";
}

private screenPositionToMapPx(
screenPosition: number,
mapStartScreen: number,
Expand Down Expand Up @@ -648,6 +692,19 @@ class RulerControl extends Control {
true,
false
);
if (tick.major && (tick.contigLabel || tick.scaffoldLabel)) {
this.drawRotatedText(
[tick.contigLabel, tick.scaffoldLabel].filter(Boolean).join(" / "),
textX,
Math.max(22, coord[1] - tickLength + 10),
context,
0,
"9px sans-serif",
textAlign,
true,
false
);
}
return;
}

Expand All @@ -663,6 +720,19 @@ class RulerControl extends Control {
true,
false
);
if (tick.major && (tick.contigLabel || tick.scaffoldLabel)) {
this.drawRotatedText(
[tick.contigLabel, tick.scaffoldLabel].filter(Boolean).join(" / "),
Math.max(2, coord[0] - tickLength - 4),
this.clamp(textY + 12, 12, this.canvas.height - 3),
context,
0,
"9px sans-serif",
"right",
true,
false
);
}
}

private formatBpLabel(bp: number, precision: number): string {
Expand Down
Loading
Loading