Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 139 additions & 0 deletions scripts/genome_display_mapper.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
extends RefCounted
class_name GenomeDisplayMapper

var chromosome_length := 1
var leading_pad_bp := 0.0
var trailing_pad_bp := 0.0
var gap_overrides: Array[Dictionary] = []
var total_bp := 1.0


func reset(length_bp: int) -> void:
chromosome_length = maxi(length_bp, 1)
leading_pad_bp = 0.0
trailing_pad_bp = 0.0
gap_overrides.clear()
total_bp = float(chromosome_length)


func recompute(length_bp: int, soft_clip_tracks: Array[Dictionary], concat_segments: Array[Dictionary]) -> void:
reset(length_bp)
if soft_clip_tracks.is_empty():
return
if concat_segments.is_empty():
_recompute_linear_padding(soft_clip_tracks)
return
_recompute_concat_padding(soft_clip_tracks, concat_segments)


func display_total_length_bp() -> float:
return maxf(total_bp, float(chromosome_length))


func display_bp_for_genome_bp(bp: float) -> float:
var out := clampf(bp, 0.0, float(chromosome_length)) + leading_pad_bp
for gap_any in gap_overrides:
var gap: Dictionary = gap_any
var next_start := float(gap.get("next_start", 0.0))
if bp >= next_start:
out += float(gap.get("extra_bp", 0.0))
return out


func genome_bp_for_display_bp(display_bp: float) -> float:
var remaining := clampf(display_bp, 0.0, display_total_length_bp()) - leading_pad_bp
if remaining <= 0.0:
return 0.0
for gap_any in gap_overrides:
var gap: Dictionary = gap_any
var next_start := float(gap.get("next_start", 0.0))
var extra_bp := float(gap.get("extra_bp", 0.0))
if remaining < next_start:
return clampf(remaining, 0.0, float(chromosome_length))
remaining -= extra_bp
if remaining < next_start:
return clampf(next_start, 0.0, float(chromosome_length))
return clampf(remaining, 0.0, float(chromosome_length))


func display_start_bp_for_view(view_start_bp: float, bp_per_px: float, plot_width_px: float) -> float:
if bp_per_px <= 0.0:
return 0.0
var raw := display_bp_for_genome_bp(view_start_bp)
if view_start_bp <= 0.0001:
raw -= leading_pad_bp
var max_display_start := maxf(0.0, display_total_length_bp() - maxf(1.0, plot_width_px) * bp_per_px)
return clampf(raw, 0.0, max_display_start)


func clamp_view_start(next_start_bp: float, bp_per_px: float, plot_width_px: float) -> float:
if plot_width_px <= 0.0:
return maxf(0.0, next_start_bp)
var max_display_start := maxf(0.0, display_total_length_bp() - plot_width_px * bp_per_px)
var next_display := display_bp_for_genome_bp(clampf(next_start_bp, 0.0, float(chromosome_length)))
return genome_bp_for_display_bp(clampf(next_display, 0.0, max_display_start))


func _recompute_linear_padding(soft_clip_tracks: Array[Dictionary]) -> void:
for entry_any in soft_clip_tracks:
var entry: Dictionary = entry_any
for read_any in entry.get("reads", []):
var read: Dictionary = read_any
var read_start := int(read.get("start", 0))
var read_end := int(read.get("end", read_start))
if read_start <= 0:
leading_pad_bp = maxf(leading_pad_bp, _soft_clip_len(read, "soft_clip_left"))
if read_end >= chromosome_length:
trailing_pad_bp = maxf(trailing_pad_bp, _soft_clip_len(read, "soft_clip_right"))
total_bp = float(chromosome_length) + leading_pad_bp + trailing_pad_bp


func _recompute_concat_padding(soft_clip_tracks: Array[Dictionary], concat_segments: Array[Dictionary]) -> void:
var left_overhangs: Dictionary = {}
var right_overhangs: Dictionary = {}
for seg_any in concat_segments:
var seg: Dictionary = seg_any
var seg_id := int(seg.get("id", -1))
left_overhangs[seg_id] = 0.0
right_overhangs[seg_id] = 0.0
for entry_any in soft_clip_tracks:
var entry: Dictionary = entry_any
for read_any in entry.get("reads", []):
var read: Dictionary = read_any
var read_start := int(read.get("start", 0))
var read_end := int(read.get("end", read_start))
var left_clip := _soft_clip_len(read, "soft_clip_left")
var right_clip := _soft_clip_len(read, "soft_clip_right")
for seg_any in concat_segments:
var seg: Dictionary = seg_any
var seg_id := int(seg.get("id", -1))
var seg_start := int(seg.get("start", 0))
var seg_end := int(seg.get("end", seg_start))
if left_clip > 0.0 and read_start <= seg_start:
left_overhangs[seg_id] = maxf(float(left_overhangs.get(seg_id, 0.0)), left_clip)
if right_clip > 0.0 and read_end >= seg_end:
right_overhangs[seg_id] = maxf(float(right_overhangs.get(seg_id, 0.0)), right_clip)
for i in range(concat_segments.size()):
var seg: Dictionary = concat_segments[i]
var seg_id := int(seg.get("id", -1))
if i == 0:
leading_pad_bp = float(left_overhangs.get(seg_id, 0.0))
if i == concat_segments.size() - 1:
trailing_pad_bp = float(right_overhangs.get(seg_id, 0.0))
continue
var next_seg: Dictionary = concat_segments[i + 1]
var next_id := int(next_seg.get("id", -1))
var seg_end := float(seg.get("end", 0))
var next_start := float(next_seg.get("start", seg_end))
var genome_gap := maxf(0.0, next_start - seg_end)
var needed_gap := float(right_overhangs.get(seg_id, 0.0)) + float(left_overhangs.get(next_id, 0.0))
var extra_gap := maxf(0.0, needed_gap - genome_gap)
if extra_gap > 0.0:
gap_overrides.append({"next_start": next_start, "extra_bp": extra_gap})
total_bp = float(chromosome_length) + leading_pad_bp + trailing_pad_bp
for gap_any in gap_overrides:
total_bp += float((gap_any as Dictionary).get("extra_bp", 0.0))


func _soft_clip_len(read: Dictionary, key: String) -> float:
return float(str(read.get(key, "")).length())
1 change: 1 addition & 0 deletions scripts/genome_display_mapper.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://cjbwh57ftgs3f
115 changes: 17 additions & 98 deletions scripts/genome_view.gd
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const AnnotationRendererScript = preload("res://scripts/annotation_renderer.gd")
const FeatureAnnotationUtilsScript = preload("res://scripts/feature_annotation_utils.gd")
const SVGCanvasScript = preload("res://scripts/svg_canvas.gd")
const MotionReadLayerScript = preload("res://scripts/motion_read_layer.gd")
const GenomeDisplayMapperScript = preload("res://scripts/genome_display_mapper.gd")
const DETAILED_READ_MAX_BP_PER_PX := 48.0

signal viewport_changed(start_bp: int, end_bp: int, bp_per_px: float)
Expand Down Expand Up @@ -242,10 +243,7 @@ var _read_loading_message := ""
var _empty_state_status := ""
var _map_drag_active := false
var _map_drag_bp_offset := 0.0
var _display_leading_pad_bp := 0.0
var _display_trailing_pad_bp := 0.0
var _display_gap_overrides: Array[Dictionary] = []
var _display_total_bp := 50000.0
var _display_mapper: RefCounted = GenomeDisplayMapperScript.new()

func _ready() -> void:
clip_contents = true
Expand Down Expand Up @@ -1148,9 +1146,7 @@ func _clamp_start(next_start: float) -> float:
var plot_w := _plot_width()
if plot_w <= 0:
return maxf(0.0, next_start)
var max_display_start := maxf(0.0, _display_total_length_bp() - plot_w * bp_per_px)
var next_display := _display_bp_for_genome_bp(clampf(next_start, 0.0, float(chromosome_length)))
return _display_bp_to_genome_bp(clampf(next_display, 0.0, max_display_start))
return _display_mapper.clamp_view_start(next_start, bp_per_px, plot_w)

func _emit_viewport_changed() -> void:
var end_bp := int(minf(float(chromosome_length), _viewport_end_bp()))
Expand Down Expand Up @@ -2667,42 +2663,28 @@ func _x_to_bp_edge(x: float) -> int:


func _current_display_start_bp() -> float:
var plot_w := _plot_width()
var raw := _display_bp_for_genome_bp(view_start_bp)
if view_start_bp <= 0.0001:
raw -= _display_leading_pad_bp
var max_display_start := maxf(0.0, _display_total_length_bp() - plot_w * bp_per_px)
return clampf(raw, 0.0, max_display_start)
return _display_mapper.display_start_bp_for_view(view_start_bp, bp_per_px, _plot_width())


func _display_start_bp_at(render_start_bp: float, render_bp_per_px: float, render_width_px: float = -1.0) -> float:
if render_bp_per_px <= 0.0:
return _current_display_start_bp()
var width_px := render_width_px
if width_px <= 0.0:
width_px = _plot_width()
return _display_mapper.display_start_bp_for_view(render_start_bp, render_bp_per_px, width_px)


func _display_total_length_bp() -> float:
return maxf(_display_total_bp, float(chromosome_length))
return _display_mapper.display_total_length_bp()


func _display_bp_for_genome_bp(bp: float) -> float:
var out := clampf(bp, 0.0, float(chromosome_length)) + _display_leading_pad_bp
for gap_any in _display_gap_overrides:
var gap: Dictionary = gap_any
var next_start := float(gap.get("next_start", 0.0))
if bp >= next_start:
out += float(gap.get("extra_bp", 0.0))
return out
return _display_mapper.display_bp_for_genome_bp(bp)


func _display_bp_to_genome_bp(display_bp: float) -> float:
var remaining := clampf(display_bp, 0.0, _display_total_length_bp()) - _display_leading_pad_bp
if remaining <= 0.0:
return 0.0
for gap_any in _display_gap_overrides:
var gap: Dictionary = gap_any
var next_start := float(gap.get("next_start", 0.0))
var extra_bp := float(gap.get("extra_bp", 0.0))
if remaining < next_start:
return clampf(remaining, 0.0, float(chromosome_length))
remaining -= extra_bp
if remaining < next_start:
return clampf(next_start, 0.0, float(chromosome_length))
return clampf(remaining, 0.0, float(chromosome_length))
return _display_mapper.genome_bp_for_display_bp(display_bp)


func _visible_soft_clip_tracks() -> Array[Dictionary]:
Expand All @@ -2728,70 +2710,7 @@ func _visible_soft_clip_tracks() -> Array[Dictionary]:


func _recompute_display_padding() -> void:
_display_leading_pad_bp = 0.0
_display_trailing_pad_bp = 0.0
_display_gap_overrides.clear()
_display_total_bp = float(max(chromosome_length, 1))
var soft_clip_tracks := _visible_soft_clip_tracks()
if soft_clip_tracks.is_empty():
return
if concat_segments.is_empty():
for entry_any in soft_clip_tracks:
var entry: Dictionary = entry_any
for read_any in entry.get("reads", []):
var read: Dictionary = read_any
var read_start := int(read.get("start", 0))
var read_end := int(read.get("end", read_start))
if read_start <= 0:
_display_leading_pad_bp = maxf(_display_leading_pad_bp, float(str(read.get("soft_clip_left", "")).length()))
if read_end >= chromosome_length:
_display_trailing_pad_bp = maxf(_display_trailing_pad_bp, float(str(read.get("soft_clip_right", "")).length()))
_display_total_bp = float(chromosome_length) + _display_leading_pad_bp + _display_trailing_pad_bp
return
var left_overhangs: Dictionary = {}
var right_overhangs: Dictionary = {}
for seg_any in concat_segments:
var seg: Dictionary = seg_any
var seg_id := int(seg.get("id", -1))
left_overhangs[seg_id] = 0.0
right_overhangs[seg_id] = 0.0
for entry_any in soft_clip_tracks:
var entry: Dictionary = entry_any
for read_any in entry.get("reads", []):
var read: Dictionary = read_any
var read_start := int(read.get("start", 0))
var read_end := int(read.get("end", read_start))
var left_clip := float(str(read.get("soft_clip_left", "")).length())
var right_clip := float(str(read.get("soft_clip_right", "")).length())
for seg_any in concat_segments:
var seg: Dictionary = seg_any
var seg_id := int(seg.get("id", -1))
var seg_start := int(seg.get("start", 0))
var seg_end := int(seg.get("end", seg_start))
if left_clip > 0.0 and read_start <= seg_start:
left_overhangs[seg_id] = maxf(float(left_overhangs.get(seg_id, 0.0)), left_clip)
if right_clip > 0.0 and read_end >= seg_end:
right_overhangs[seg_id] = maxf(float(right_overhangs.get(seg_id, 0.0)), right_clip)
for i in range(concat_segments.size()):
var seg: Dictionary = concat_segments[i]
var seg_id := int(seg.get("id", -1))
if i == 0:
_display_leading_pad_bp = float(left_overhangs.get(seg_id, 0.0))
if i == concat_segments.size() - 1:
_display_trailing_pad_bp = float(right_overhangs.get(seg_id, 0.0))
continue
var next_seg: Dictionary = concat_segments[i + 1]
var next_id := int(next_seg.get("id", -1))
var seg_end := float(seg.get("end", 0))
var next_start := float(next_seg.get("start", seg_end))
var genome_gap := maxf(0.0, next_start - seg_end)
var needed_gap := float(right_overhangs.get(seg_id, 0.0)) + float(left_overhangs.get(next_id, 0.0))
var extra_gap := maxf(0.0, needed_gap - genome_gap)
if extra_gap > 0.0:
_display_gap_overrides.append({"next_start": next_start, "extra_bp": extra_gap})
_display_total_bp = float(chromosome_length) + _display_leading_pad_bp + _display_trailing_pad_bp
for gap_any in _display_gap_overrides:
_display_total_bp += float((gap_any as Dictionary).get("extra_bp", 0.0))
_display_mapper.recompute(chromosome_length, _visible_soft_clip_tracks(), concat_segments)

func _nice_tick(raw: float) -> float:
if raw <= 0.0:
Expand Down
11 changes: 1 addition & 10 deletions scripts/read_track_renderer.gd
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,7 @@ func _draw_set_transform_on(target, offset: Vector2, rotation: float, scale: Vec


func _display_start_bp_at(render_start_bp: float, render_bp_per_px: float, render_width_px: float = -1.0) -> float:
if render_bp_per_px <= 0.0:
return view._current_display_start_bp()
var width_px := render_width_px
if width_px <= 0.0:
width_px = maxf(1.0, view.size.x - view.TRACK_LEFT_PAD - view.TRACK_RIGHT_PAD)
var raw := view._display_bp_for_genome_bp(render_start_bp)
if render_start_bp <= 0.0001:
raw -= view._display_leading_pad_bp
var max_display_start := maxf(0.0, view._display_total_length_bp() - width_px * render_bp_per_px)
return clampf(raw, 0.0, max_display_start)
return view._display_start_bp_at(render_start_bp, render_bp_per_px, render_width_px)


func _viewport_start_bp_at(render_start_bp: float, render_bp_per_px: float) -> float:
Expand Down
1 change: 1 addition & 0 deletions tests/godot/run_tests.gd
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ extends SceneTree

const TEST_SCRIPTS := [
"res://tests/godot/test_annotation_cache_controller.gd",
"res://tests/godot/test_genome_display_mapper.gd",
"res://tests/godot/test_local_zem_manager.gd",
"res://tests/godot/test_track_controls_controller.gd",
"res://tests/godot/test_track_settings_controller.gd",
Expand Down
72 changes: 72 additions & 0 deletions tests/godot/test_genome_display_mapper.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
extends "res://tests/godot/test_case.gd"

const GenomeDisplayMapperScript = preload("res://scripts/genome_display_mapper.gd")

var mapper: RefCounted


func setup() -> void:
mapper = GenomeDisplayMapperScript.new()


func teardown() -> void:
mapper = null


func test_linear_soft_clips_extend_display_edges() -> void:
var tracks: Array[Dictionary] = [
{
"reads": [
{"start": 0, "end": 20, "soft_clip_left": "AAAA"},
{"start": 80, "end": 100, "soft_clip_right": "TT"}
]
}
]
var segments: Array[Dictionary] = []

mapper.recompute(100, tracks, segments)

assert_eq(mapper.leading_pad_bp, 4.0)
assert_eq(mapper.trailing_pad_bp, 2.0)
assert_eq(mapper.display_total_length_bp(), 106.0)
assert_eq(mapper.display_bp_for_genome_bp(0), 4.0)
assert_eq(mapper.genome_bp_for_display_bp(3.0), 0.0)
assert_eq(mapper.genome_bp_for_display_bp(106.0), 100.0)


func test_concat_soft_clips_insert_gap_between_adjacent_segments() -> void:
var tracks: Array[Dictionary] = [
{
"reads": [
{"start": 80, "end": 100, "soft_clip_right": "TTTTTTTTTT"},
{"start": 100, "end": 120, "soft_clip_left": "AAAAA"}
]
}
]
var segments: Array[Dictionary] = [
{"id": 1, "start": 0, "end": 100},
{"id": 2, "start": 100, "end": 200}
]

mapper.recompute(200, tracks, segments)

assert_eq(mapper.leading_pad_bp, 0.0)
assert_eq(mapper.trailing_pad_bp, 0.0)
assert_eq(mapper.gap_overrides.size(), 1)
assert_eq(mapper.gap_overrides[0], {"next_start": 100.0, "extra_bp": 15.0})
assert_eq(mapper.display_total_length_bp(), 215.0)
assert_eq(mapper.display_bp_for_genome_bp(99), 99.0)
assert_eq(mapper.display_bp_for_genome_bp(100), 115.0)
assert_eq(mapper.genome_bp_for_display_bp(105.0), 100.0)


func test_display_start_uses_leading_pad_only_at_left_boundary() -> void:
var tracks: Array[Dictionary] = [
{"reads": [{"start": 0, "end": 10, "soft_clip_left": "AAAA"}]}
]
var segments: Array[Dictionary] = []

mapper.recompute(100, tracks, segments)

assert_eq(mapper.display_start_bp_for_view(0.0, 10.0, 5.0), 0.0)
assert_eq(mapper.display_start_bp_for_view(10.0, 10.0, 5.0), 14.0)
Loading
Loading