Newer
Older
theme = Dict(:LarvaPlot => Dict(
:path_color => :lightgrey,
:path_linewidth => 1,
:shape_linewidth => 2,
:shape_alpha => 0.1f0,
:fallback_color => :lightgrey,
))
@recipe(LarvaPlot) do scene
Theme(theme[:LarvaPlot]...)
#
const CompatLarvaID = Int16
const ActiveLarva = Union{Nothing, CompatLarvaID}
function withalpha(outline_color, alpha_value)
color = to_color(outline_color)
return RGBA(color.r, color.g, color.b, alpha_value)
end
withalpha(outline_color::Observable, alpha_value::Observable) = lift(withalpha, outline_color, alpha_value)
const MinimalLarva = Tuple{PathOrOutline, PathOrOutline, Color, Bool}
const wgl = startswith(string(Makie.current_backend[]), "WGL")
function Makie.plot!(plot::LarvaPlot{<:MinimalLarva})
path = plot[1]
shape_outline = plot[2]
shape_color = plot[3]
visibility = plot[4]
lines!(plot, path;
color=plot.path_color,
linewidth=plot.path_linewidth,
lines!(plot, shape_outline;
color=shape_color,
linewidth=plot.shape_linewidth,
translate!(plot.plots[end], Makie.Vec3f(0, 0, 1))
if !wgl
poly!(plot, shape_outline;
color=withalpha(shape_color, plot.shape_alpha),
transparency=true,
visible=visibility)
translate!(plot.plots[end], Makie.Vec3f(0, 0, 0.5))
end
"""
Larva controller.
"""
struct StatefulLarva
# model side
model::LarvaModel
tag_lut::AbstractObservable{<:TagLUT}
timestep::AbstractObservable{Int}
visible::AbstractObservable{Bool}
# view side
shape_outline::AbstractObservable{PathOrOutline}
shape_color::AbstractObservable{<:Color}
end
function StatefulLarva(larva::LarvaModel,
tag_lut::AbstractObservable{<:TagLUT},
timestep::AbstractObservable{Int};
fallback_color::OptionalColor=nothing,
)
timeseries = larva.fullstates
alignedsteps = larva.alignedsteps
first_timestep = alignedsteps[1]
last_timestep = alignedsteps[end]
fallback_color = @something fallback_color theme[:LarvaPlot][:fallback_color]
_, color = gettag(tag_lut, larva, larva.alignedsteps[1], fallback_color)
# key observables
shape_outline = Observable(outline(Makie.Point2f, state))
onany(timestep, tag_lut, larva.usertags) do timestep, tag_lut, _
if first_timestep<=timestep && timestep<=last_timestep
step = timestep - first_timestep + 1
_, state = timeseries[step]
shape_outline.val = outline(Makie.Point2f, state)
#
_, color = gettag(tag_lut, larva, timestep, fallback_color)
visibility[] = true # notify all subplots
elseif visibility[]
visibility[] = false
end
end
StatefulLarva(larva, tag_lut, timestep, visibility, shape_outline, shape_color)
end
function Makie.plot!(plot::LarvaPlot{Tuple{StatefulLarva}})
larva = plot[1][]
path = larva.model.path
shape_outline = larva.shape_outline
shape_color = larva.shape_color
visibility = larva.visible
larvaplot!(plot, path, shape_outline, shape_color, visibility)
end
controller
model::Observable
usertags::Observable
visible::Observable{Bool}
path::Observable{PathOrOutline}
outline::Observable{PathOrOutline}
outline_color::Observable{String}
segment::Union{Nothing, Observable{PathOrOutline}}
segment_color::Observable{String}
segment_visible::Observable{Bool}
function SingleLarvaView(larvae::Vector{LarvaModel}, controller; editabletags::Bool=true)
fallback_color = theme[:LarvaPlot][:fallback_color]
# initial values
larva = larvae[1]
_, state = larva.fullstates[1]
_, color = gettag(gettags(controller), larva, larva.alignedsteps[1], fallback_color)
# observables
model = Observable(larva)
usertags = Observable(larva.usertags)
visible = Observable(false)
path = Observable(larva.path)
shape_outline = Observable(outline(Makie.Point2f, state))
shape_color = Observable(html_color(color))
# TODO: move the callbacks to `plot!`
if isnothing(id)
visible[] = false
else
for larva in larvae
if id == larva.id
model[] = larva
break
end
end
end
end
tags = gettags(controller)
timestep = gettimestep(controller)
f = on(usertags[]) do _
_, color = gettag(tags[], model[], timestep[], fallback_color)
shape_color[] = html_color(color)
end
on(model) do larva
path[] = larva.path
usertags[] = larva.usertags
#
Observables.off(f)
f = on(larva.usertags) do _
_, color = gettag(tags[], larva, timestep[], fallback_color)
shape_color[] = html_color(color)
end
usertags,
) do larva, timestep, tag_lut, usertags
first_timestep = larva.alignedsteps[1]
last_timestep = larva.alignedsteps[end]
if (first_timestep<=timestep &&
timestep<=last_timestep &&
getactivelarva(controller)[] == model[].id
)
step = timestep - first_timestep + 1
_, state = larva.fullstates[step]
shape_outline.val = outline(Makie.Point2f, state)
#
_, color = gettag(tag_lut, larva, timestep, fallback_color)
visible[] = true # notify all subplots
elseif visible[]
visible[] = false
end
end
segment = editabletags ? Observable(PathOrOutline()) : nothing
segment_color = Observable(shape_color[])
segment_visible = Observable(false)
SingleLarvaView(controller, model, usertags, visible,
path, shape_outline, shape_color,
segment, segment_color, segment_visible)
iseditable(larvaview::SingleLarvaView) = !isnothing(larvaview.segment)
norm2(p, q) = @. norm2(p - q)
norm2(p) = p[1]^2 + p[2]^2
function pick_timestep(scene, larva)
pos = mouseposition(scene)
dist2 = norm2(larva.path[], pos)
best = argmin(dist2)
timestep = larva.model[].alignedsteps[best]
return timestep, best, dist2[best]
function setinitialstep!(initialstep, larva)
step = initialstep[]
if step == 0
timestep = gettimestep(larva.controller)[]
firststep = larva.model[].alignedsteps[1]
if timestep < firststep
@warn "Current time step is earlier than current track"
timestep = firststep
end
# faster but possibly wrong:
step = timestep - firststep + 1 #findfirst(larva.model[].alignedsteps == timestep)
initialstep[] = step
end
return step
end
function pick_timesegment(scene, larva, initialstep)
timestep, best, dist2 = pick_timestep(scene, larva)
if initialstep isa Ref
initialstep = initialstep[]
end
segment = initialstep <= best ? path[initialstep:best] : path[best:initialstep]
return timestep, segment, dist2
end
function assign_tag_to_segment!(larvaview, firststep)
segment_color = larvaview.segment_color[]
controller = larvaview.controller
larva = larvaview.model[]
firststep′ = larva.alignedsteps[firststep]
laststep′ = gettimestep(controller)[]
laststep = laststep′ - larva.alignedsteps[1] + 1
firststep″, laststep″ = min(firststep, laststep), max(firststep, laststep)
# history
previous_usertags = freezetags(larva.usertags[],
larva.alignedsteps[firststep″],
larva.alignedsteps[laststep″])
lut = gettags(controller)
usertags = getusertags!(larva, firststep′; lut=lut)
tag = gettag(usertags, lut)
if isnothing(tag)
@info "Removing all tags on segment" t0=larva.fullstates[firststep][1] t1=larva.fullstates[laststep][1]
# history
transaction = RemoveAllTags(larva.id,
(firststep′, laststep′),
previous_usertags)
#
for step in firststep″:laststep″
usertags = getusertags!(larva, larva.alignedsteps[step]; lut=lut)
deletetags!(usertags)
end
else
@assert segment_color == tag.color[]
@info "Assigning tag to segment" tag=tag.name[] t0=larva.fullstates[firststep][1] t1=larva.fullstates[laststep][1]
# history
transaction = OverrideTags(larva.id,
(firststep′, laststep′),
tag.name[],
previous_usertags)
#
for step in firststep″:laststep″
usertags = getusertags!(larva, larva.alignedsteps[step]; lut=lut)
push!(transactions(controller), transaction)
Observables.notify(larva.usertags)
Observables.notify(larvaview.usertags)
Observables.notify(gettimestep(controller)) # full refresh
flag_active_larva_as_edited(controller)
end
function setmouseevents!(scene, larva::SingleLarvaView; blocking=true, priority=Int8(10), kwargs...)
# WGLMakie does not support addmouseevents! with plots as additional input args
mouseevents = Makie.addmouseevents!(scene; priority=priority)
Makie.onmouseleftclick(mouseevents) do _
timestep, _, _ = pick_timestep(scene, larva)
settimestep!(larva.controller, timestep)
return Consume(false)
end
reference_larva_size = getmedianlarvasize(larva.controller)
# dead code
modifier_active() = ispressed(scene, Keyboard.left_shift)
function larva_hovered()
currentstep = gettimestep(larva.controller)[]
firststep = larva.model[].alignedsteps[1]
centroid = larva.path[][currentstep-firststep+1]
pointer = mouseposition(scene)
return norm2(centroid - pointer) <= reference_larva_size^2
dragging = Ref(false)
initialstep = Ref(0)
dragstart = Ref(0.0)
dragstart[] = time()
dragging[] = true
setinitialstep!(initialstep, larva)
if iseditable(larva)
larva.segment_visible[] = false
larva.segment[] = PathOrOutline()
larva.segment_color[] = larva.outline_color[]
end
end
function reset()
dragging[] = false
initialstep[] = 0
if iseditable(larva)
larva.segment_visible[] = false
larva.segment[] = PathOrOutline()
end
end
stop = reset
Makie.onmouseleftdragstart(mouseevents) do _
@debug "Start to drag"
start()
return Consume(blocking)
end
return Consume(false)
end
Makie.onmouseleftdrag(mouseevents) do _
if iseditable(larva)
timestep, segment, dist2 = pick_timesegment(scene, larva, initialstep)
settimestep!(larva.controller, timestep)
if dist2 > (2reference_larva_size)^2
assignmentfailed!(larva.controller, "mouse pointer away")
stop()
else
if !(isempty(segment) || larva.segment_visible[])
larva.segment_visible[] = true
end
larva.segment[] = segment
timestep, _, _ = pick_timestep(scene, larva)
settimestep!(larva.controller, timestep)
end
end
Makie.onmouseleftdragstop(mouseevents) do _
if iseditable(larva)
if time() - dragstart[] > 1
assign_tag_to_segment!(larva, initialstep[])
else
assignmentfailed!(larva.controller, "drag duration too short")
end
end
return Consume(false)
end
on(taggingevents(larva.controller).assignmentfailed) do reason
@info "Tag assignment cancelled ($reason)"
end
end
function Makie.plot!(plot::LarvaPlot{Tuple{SingleLarvaView}})
larva = plot[1][]
scene = plot.parent
setmouseevents!(scene, larva)
larvaplot!(plot, larva.path, larva.outline, larva.outline_color, larva.visible)
if iseditable(larva)
lines!(plot, larva.segment; color=larva.segment_color, linewidth=6, visible=larva.segment_visible)
translate!(plot.plots[end], Makie.Vec3f(0, 0, 0.1))
end
struct DecoratedLarva
larva::StatefulLarva
activearea
decorated::AbstractObservable{Bool}
function DecoratedLarva(larva::StatefulLarva)
decorated = Observable(false)
on(larva.visible) do visible
# on larva disappearance
if !visible && decorated[]
decorated[] = false
end
end
l, r = extrema([p[1] for p in larva.model.path])
b, t = extrema([p[2] for p in larva.model.path])
margin = theme[:DecoratedLarva][:decoration_margin]
if 0 < margin
l -= margin
b -= margin
r += margin
t += margin
bl, tr = Meshes.Point2f(l, b), Meshes.Point2f(r, t)
br, tl = Meshes.Point2f(r, b), Meshes.Point2f(l, t)
activearea = Meshes.Quadrangle(bl, br, tr, tl)
DecoratedLarva(larva, activearea, decorated)
function DecoratedLarva(larva::LarvaModel, args...; kwargs...)
DecoratedLarva(StatefulLarva(larva, args...; kwargs...))
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
theme[:DecoratedLarva] = Dict(:hover_color => :red,
:hover_linewidth => 4,
:decoration_margin => 2.0f0,
)
function Makie.plot!(plot::LarvaPlot{Tuple{DecoratedLarva}})
decoratedlarva = plot[1][]
decorated = decoratedlarva.decorated
larva = decoratedlarva.larva
# on(decorated) do b
# if b
# @info "larva #$(larva.model.id) decorated"
# end
# end
# plot
larvaplot!(plot, larva)
#decoratedlarva.primitive_child = plot.plots[1]
# decoration
outline = Vector(vertices(decoratedlarva.activearea))
p = Makie.Point2f ∘ coordinates
close!(path) = push!(path, path[1])
lines!(plot, close!(p.(outline));
color=theme[:DecoratedLarva][:hover_color],
linewidth=theme[:DecoratedLarva][:hover_linewidth],
visible=decorated,
)
# TODO: plot larva ID?
end
struct DecoratedLarvae
larvae::Vector{DecoratedLarva}
centers::Array{Float32, 2}
norm2::Array{Float32, 2}
hovered_larva::AbstractObservable{Int}
hovering_active::AbstractObservable{Bool}
end
function DecoratedLarvae(larvae::Vector{LarvaModel}, args...; kwargs...)
larvae′ = DecoratedLarva[]
for larva in larvae
try
push!(larvae′, DecoratedLarva(larva, args...; kwargs...))
catch e
@warn "Skipping track on failure" id=larva.id cause=e
end
end
DecoratedLarvae(larvae′)
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
function DecoratedLarvae(larvae::Vector{DecoratedLarva})
# not-a-larva is encoded as index 0
current_larva = Observable(0)
hovered_larva = Observable(0)
hovering_active = Observable(true)
on(hovering_active) do active
if !(active || isnothing(hovered_larva[]))
hovered_larva[] = 0
end
end
# note: if an active larva turns invisible, decorations will disappear,
# but the above observables won't be cleared
on(hovered_larva) do j
@assert hovering_active[]
i = current_larva[]
if i == j
@warn "moving too fast?"
end
if 0 < i
i_decorated = larvae[i].decorated
if i_decorated[]
i_decorated[] = false
else
@warn begin
i_id = larvae[i].larva.model.id
"larva #$(i_id)'s decorations already removed"
end
end
end
current_larva.val = j
if 0 < j
if larvae[j].larva.visible[]
larvae[j].decorated[] = true
else
@error begin
j_id = larvae[j].larva.model.id
"cannot decorate invisible larva #$(j_id)"
end
hovered_larva.val = 0
end
end
end
center = Meshes.coordinates ∘ Meshes.centroid
centers = cat((center(larva.activearea) for larva in larvae)...; dims=2)
norm2 = sum(centers .* centers, dims=1)
DecoratedLarvae(larvae, centers, norm2, hovered_larva, hovering_active)
end
function find(larvae::DecoratedLarvae, position)
pos = Vector(position)'
p2 = pos * pos'
visible = [i for (i, larva) in enumerate(larvae.larvae) if larva.larva.visible[]]
isempty(visible) && return 0
dist2 = vec(larvae.norm2[:,visible] .+ p2 .- 2 * pos * larvae.centers[:,visible])
bestlarva = visible[argmin(dist2)]
if Meshes.Point2f(pos...) ∉ larvae.larvae[bestlarva].activearea
# not-a-larva is encoded as index 0
bestlarva = 0
end
return bestlarva
end
function Makie.plot!(plot::LarvaPlot{Tuple{DecoratedLarvae}})
content = plot[1][]
for larva in content.larvae
larvaplot!(plot, larva)
end
scene = plot.parent
on(events(scene).mouseposition) do _
#pos = Makie.to_world(scene, Makie.screen_relative(scene, mp))
pos = mouseposition(scene)
larva = find(content, pos)
if larva != content.hovered_larva[]
content.hovered_larva[] = larva
end
end
return Consume(false)
end
end
Meshes.boundingbox(larva::StatefulLarva) = Meshes.boundingbox(larva.model)
Meshes.boundingbox(larva::DecoratedLarva) = Meshes.boundingbox(larva.larva)
Meshes.boundingbox(larvae::DecoratedLarvae) = Meshes.boundingbox(map(Meshes.boundingbox, larvae.larvae))
function setmouseevents!(scene, plot::DecoratedLarvae, ctrl; consume=false)
consumed = consume
if mb.button == Mouse.left && mb.action == Mouse.release
larva_ix = plot.hovered_larva[]
if 0 < larva_ix
larva_id = plot.larvae[larva_ix].larva.model.id
activatelarva!(ctrl, larva_id)
consumed = true
end
end
return Consume(consumed)
end