From d99771045efdde12ad1ba0fcf155cfac69daae63 Mon Sep 17 00:00:00 2001 From: qwjyh Date: Sat, 6 Jan 2024 23:46:51 +0900 Subject: [PATCH] 2d visualization --- interactive_viz.jl | 205 ++++++++++++++++++++++++++++++++ src/CoordVisualize.jl | 2 + src/parser.jl | 4 +- src/recipes.jl | 264 ++++++++++++++++++++++++++++++++++++++++++ src/visualize.jl | 212 +-------------------------------- 5 files changed, 474 insertions(+), 213 deletions(-) create mode 100644 interactive_viz.jl create mode 100644 src/recipes.jl diff --git a/interactive_viz.jl b/interactive_viz.jl new file mode 100644 index 0000000..c37fed8 --- /dev/null +++ b/interactive_viz.jl @@ -0,0 +1,205 @@ +true || include("src/CoordVisualize.jl") +true || using GLMakie +using CoordVisualize +using FileIO +using ColorSchemes +using ColorTypes + +mappath = "map.png" +map = load(mappath) +map_height, map_width = size(map) + +fig = Figure(; size = (1000, 700)) +ax = Axis( + fig[1:2, 1], + limits = ( + -map_width ÷ 2 * 1.1, + map_width ÷ 2 * 1.1, + -map_height ÷ 2 * 1.1, + map_height ÷ 2 * 1.1, + ), + aspect = DataAspect(), +) + +# Options +options_width = 200 +button_reset = Button(fig, label = "reset view") +toggle_inspector = Toggle(fig, active = false, tellwidth = false) +menu_lcolormapfunc = + Menu(fig, options = ["log", "altitude", "date", "constant"], default = "log") +menu_mcolormapfunc = + Menu(fig, options = ["log", "altitude", "date", "constant"], default = "log") +toggle_line = Toggle(fig, active = true, tellwidth = false) +toggle_marker = Toggle(fig, active = false, tellwidth = false) +slider_linewidth = Slider(fig, range = unique([1:1:5..., 5:2:15..., 15, 20:10:100...])) +slider_markersize = Slider(fig, range = unique([1:1:10..., 10:5:100...]), startvalue = 5) +# menu_linecolormap = +# Menu(fig, options = string.(keys(ColorSchemes.colorschemes)), default = "viridis") +# menu_markercolormap = +# Menu(fig, options = string.(keys(ColorSchemes.colorschemes)), default = "viridis") +textbox_linecolormap = + Textbox(fig, validator = (s -> s in string.(keys(ColorSchemes.colorschemes)))) +textbox_markercolormap = + Textbox(fig, validator = (s -> s in string.(keys(ColorSchemes.colorschemes)))) +textbox_linecolor = Textbox(fig, validator = (s -> begin + try + parse(Colorant, s) + catch + return false + end + return true +end), stored_string = "green") +lineconstcolor = + @lift(ColorMapFuncs.Constant(parse(Colorant, $(textbox_linecolor.stored_string)))) +textbox_markercolor = Textbox(fig, validator = (s -> begin + try + parse(Colorant, s) + catch + return false + end + return true +end), stored_string = "green") +markerconstcolor = + @lift(ColorMapFuncs.Constant(parse(Colorant, $(textbox_markercolor.stored_string)))) +line_options = grid!( + [1, 1] => Label(fig, "color"), + [1, 2:3] => menu_lcolormapfunc, + [2, 1] => Label(fig, "show line"), + [2, 2:3] => toggle_line, + [3, 1] => Label(fig, "width"), + [3, 2] => slider_linewidth, + [3, 3] => Label(fig, @lift(string($(slider_linewidth.value)))), + [4, 1] => Label(fig, "color scheme"), + [4, 2:3] => textbox_linecolormap, + [5, 1] => Label(fig, "color"), + [5, 2:3] => textbox_linecolor, + width = options_width, +) +marker_options = grid!( + [1, 1] => Label(fig, "color"), + [1, 2:3] => menu_mcolormapfunc, + [2, 1] => Label(fig, "show marker"), + [2, 2:3] => toggle_marker, + [3, 1] => Label(fig, "size"), + [3, 2] => slider_markersize, + [3, 3] => Label(fig, @lift(string($(slider_markersize.value)))), + [4, 1] => Label(fig, "color scheme"), + [4, 2:3] => textbox_markercolormap, + [5, 1] => Label(fig, "color"), + [5, 2:3] => textbox_markercolor, + width = options_width, +) +inspector_options = grid!( + [1, 1] => Label(fig, "inspector"), + [1, 2] => toggle_inspector, + width = options_width, +) +fig[1:2, 2] = grid!( + [0, :] => Label(fig, "Line", font = :bold), + [1, :] => line_options, + [2, :] => Label(fig, "Marker", font = :bold), + [3, :] => marker_options, + [4, :] => Label(fig, "Axis", font = :bold), + [5, :] => inspector_options, + [6, :] => button_reset, + tellheight = false, + width = options_width, +) + +tlog = vcat(CoordVisualize.parse_log.(["coord_log_5.txt", "coord_log_6.txt"])...) + +# Main +heatmap!( + ax, + (1:map_width) .- map_width ÷ 2 .- 1, + (1:map_height) .- map_height ÷ 2 .- 1, + rotr90(map), + inspectable = false, +) + +tr2d = CoordVisualize.trace2ds!( + ax, + tlog, + linewidth = slider_linewidth.value, + markersize = slider_markersize.value, +) + +# legend +cbl = Colorbar( + fig, + colormap = tr2d.linecolormap, + ticks = tr2d.lcolorticks, + # ticklabelrotation = π / 2, + ticklabelsize = 10, + label = menu_lcolormapfunc.selection, + # vertical = false, + # flipaxis = false, +) +cbm = Colorbar( + fig, + colormap = tr2d.markercolormap, + ticks = tr2d.mcolorticks, + # ticklabelrotation = π / 2, + ticklabelsize = 10, + label = menu_mcolormapfunc.selection, +) +fig[1:2, 3] = grid!( + [0, 1] => Label(fig, "line", font = :bold), + [1, 1] => cbl, + [2, 1] => Label(fig, "marker", font = :bold), + [3, 1] => cbm, + tellheight = true, +) + +inspector = DataInspector(tr2d) +inspector.attributes.enabled[] = false + +on(menu_lcolormapfunc.selection) do s + if s == "log" + tr2d.lcolormapfunc[] = ColorMapFuncs.ColorMap() + elseif s == "altitude" + tr2d.lcolormapfunc[] = ColorMapFuncs.Altitude() + elseif s == "date" + tr2d.lcolormapfunc[] = ColorMapFuncs.Date() + elseif s == "constant" + tr2d.lcolormapfunc[] = lineconstcolor[] + end +end +on(lineconstcolor) do c + tr2d.lcolormapfunc[] = c +end +on(menu_mcolormapfunc.selection) do s + if s == "log" + tr2d.mcolormapfunc[] = ColorMapFuncs.ColorMap() + elseif s == "altitude" + tr2d.mcolormapfunc[] = ColorMapFuncs.Altitude() + elseif s == "date" + tr2d.mcolormapfunc[] = ColorMapFuncs.Date() + elseif s == "constant" + tr2d.mcolormapfunc[] = markerconstcolor[] + end +end +on(markerconstcolor) do c + tr2d.mcolormapfunc[] = c +end +on(button_reset.clicks) do n + reset_limits!(ax) + # slider_markersize.value[] = 5 + # slider_linewidth.value[] = 1 + # toggle_line.active[] = true + # toggle_marker.active[] = false +end +on(textbox_linecolormap.stored_string) do s + tr2d.linecolormap[] = ColorSchemes.colorschemes[Symbol(s)] +end +on(textbox_markercolormap.stored_string) do s + tr2d.markercolormap[] = ColorSchemes.colorschemes[Symbol(s)] +end +on(toggle_inspector.active) do f + inspector.attributes.enabled[] = f +end + +connect!(tr2d.showline, toggle_line.active) +connect!(tr2d.showmarker, toggle_marker.active) + +display(fig) diff --git a/src/CoordVisualize.jl b/src/CoordVisualize.jl index 213e425..74b0457 100644 --- a/src/CoordVisualize.jl +++ b/src/CoordVisualize.jl @@ -3,11 +3,13 @@ module CoordVisualize using Dates export CoordLog +export ColorMapFuncs include("typedef.jl") include("parser.jl") include("edit.jl") include("print.jl") +include("recipes.jl") include("visualize.jl") end # module CoordVisualize diff --git a/src/parser.jl b/src/parser.jl index 3b9d5e5..8881a97 100644 --- a/src/parser.jl +++ b/src/parser.jl @@ -11,10 +11,10 @@ If keyword argument `interactive` is set to `true`, prompts will be shown to receive custom notes for each `CoordLog`. Otherwise the note is set to ""(empty string). """ -function parse_log(filepath::AbstractString; interactive=false)::Vector{CoordLog} +function parse_log(filepath::AbstractString; interactive=false)::Vector{CoordLog{Float64}} istracing::Bool = false coords_trace = Vector{Vector{Float64}}(undef, 0) # SVector ? - ret = Vector{CoordLog}() + ret = Vector{CoordLog{Float64}}() log_date = DateTime(0) for (i, l) in enumerate(readlines(filepath)) # skip logs not from tracecoord diff --git a/src/recipes.jl b/src/recipes.jl new file mode 100644 index 0000000..b18f1ea --- /dev/null +++ b/src/recipes.jl @@ -0,0 +1,264 @@ +using GLMakie +using ColorTypes +using ColorSchemes + +""" +Predefined color map functions. + +# Types + +[`ColorMapFunc`](@ref) + +# Interface + +Define these methods for the ColorMapFunc. + + (AbstractVector{CoordLog}) -> Vector{∈ [0, 1]}, ticks +""" +module ColorMapFuncs + +using ..CoordVisualize: CoordLog, n_coords +using ColorTypes +using Dates: DateTime, DateFormat, @dateformat_str, format +using Makie: wong_colors, Scene + +""" +# Methods + (f::ColorMapFunc)(cmap, logs) + +Helper method. +""" +abstract type ColorMapFunc end + +function (f::ColorMapFunc)(cmap, logs, n) + steps, ticklabels = f(logs, n) + ticks = collect(LinRange(0, 1, n)) + return get.(Ref(cmap), steps), (ticks, ticklabels) +end + +"Use same color." +struct Constant <: ColorMapFunc + color::Colorant +end + +Constant(c::Symbol) = Constant(parse(Colorant, c)) + +function (c::Constant)(map, logs, n) + # Iterators.repeated(c.color, length(logs)) + fill(c.color, sum(n_coords, logs)), ([], []) +end + +"Use colormap." +struct ColorMap <: ColorMapFunc + colormap::AbstractVector{<:Colorant} +end + +ColorMap() = ColorMap(wong_colors()) +ColorMap(cmap::Vector{Symbol}) = ColorMap(map(cmap) do s + parse(Colorant, s) +end) +function ColorMap(scene::Scene) + ColorMap(theme(scene, :linecolor)) +end + +function (cm::ColorMap)(map, logs, n) + cm = Iterators.cycle(cm.colormap) + nlog_s = Iterators.map(n_coords, logs) + colors = + Iterators.map(zip(cm, nlog_s)) do (color, count) + Iterators.repeated(color, count) + end |> Iterators.flatten |> collect + return colors, ([], []) +end + +"Color depending on log date." +struct Date <: ColorMapFunc end + +function (::Date)(logs::AbstractVector{CoordLog{T}}, n) where {T} + dformat = dateformat"yyyy-m-d" + logdates::Vector{DateTime} = map(logs) do log + fill(log.logdate, n_coords(log)) + end |> (v -> vcat(v...)) + fst, lst = extrema(logdates) + normeddate = (logdates .- fst) ./ (lst - fst) + diff = (lst - fst) / (n - 1) + ticklabels = format.(fst:diff:lst, dformat) + return normeddate, ticklabels +end + +"Color depending on altitude." +struct Altitude <: ColorMapFunc end + +function (f::Altitude)(logs::AbstractVector{CoordLog{T}}, n) where {T} + altitudes = map(logs) do log + Iterators.map(eachrow(log.coords)) do c + c[2] + end + end |> Iterators.flatten |> collect + low, high = extrema(altitudes) + normedalt = (altitudes .- low) ./ (high - low) + ticklabels = string.(round.(LinRange(low, high, n))) + return normedalt, ticklabels +end + +end # module ColorMapFunc + +# TODO: alpha? +""" + trace2ds(log::Vector{CoordLog}) + +# Arguments +TODO +""" +@recipe(Trace2Ds, log) do scene + Attributes(; + showmarker = false, + marker = theme(scene, :marker), + markercolormap = theme(scene, :colormap), + markersize = theme(scene, :markersize), + strokewidth = 0, + showline = true, + linecolormap = theme(scene, :colormap), + linestyle = theme(scene, :linestyle), + linewidth = theme(scene, :linewidth), + inspectable = theme(scene, :inspectable), + lcolormapfunc = ColorMapFuncs.ColorMap(), # or func like in ColorMapFunc + mcolormapfunc = ColorMapFuncs.ColorMap(), + lcolorticks = nothing, + nlcolorticks = 5, + mcolorticks = nothing, + nmcolorticks = 5, + ) +end + +function Makie.plot!(tr2d::Trace2Ds) + # @info "logs" tr2d + # @info "fieldnames" tr2d.log + # @info "" theme(tr2d, :colormap) + + lcolormapfunc = tr2d.lcolormapfunc + + ntraces = length(tr2d.log[]) # number of CoordLog + linesegs = Observable(Point2f[]) + points = Observable(Point2f[]) + altitudes = Observable(Float64[]) + notes = Observable(String[]) + if tr2d.markercolormap[] isa Symbol + tr2d.markercolormap[] = getproperty(ColorSchemes, tr2d.markercolormap[]) + end + markercolors = Observable( + tr2d.mcolormapfunc[](tr2d.markercolormap[], tr2d.log[], tr2d.nmcolorticks[])[1], + ) + mticks = tr2d.mcolorticks + if tr2d.linecolormap[] isa Symbol + tr2d.linecolormap[] = getproperty(ColorSchemes, tr2d.linecolormap[]) + end + # @info "lcolormapfunc" lcolormapfunc + linecolors = + Observable(lcolormapfunc[](tr2d.linecolormap[], tr2d.log[], tr2d.nlcolorticks[])[1]) + lticks = tr2d.lcolorticks + + # helper function which mutates observables + function update_plot( + logs::AbstractVector{<:CoordLog{T}}, + lcolormap, + mcolormap, + lcolormapfunc, #::Union{Symbol, Tuple{Symbol, Symbol}}, + mcolormapfunc, + ) where {T} + @info "update_plot" + markercolors[] + linecolors[] + # @info "logs on update_plot" logs + # init + empty!(linesegs[]) + empty!(points[]) + empty!(altitudes[]) + empty!(markercolors[]) + if linecolors[] isa AbstractVector + empty!(linecolors[]) + else + linecolors[] = [] + end + + # update + colors_count = 1 + lcolors, lticks[] = lcolormapfunc(lcolormap, logs, tr2d.nlcolorticks[]) + mcolors, mticks[] = mcolormapfunc(mcolormap, logs, tr2d.nmcolorticks[]) + for (i, log) in enumerate(logs) + first = true + for point in eachrow(log.coords) + push!(linesegs[], Point2f(point[1], point[3])) + push!(linesegs[], Point2f(point[1], point[3])) + push!(points[], Point2f(point[1], point[3])) + push!(altitudes[], point[2]) + push!(linecolors[], lcolors[colors_count]) + push!(linecolors[], lcolors[colors_count]) + push!(markercolors[], mcolors[colors_count]) + colors_count += 1 + + # # marker + # if !isnothing(mcolormapfunc) + # push!(markercolors[], mcolormapfunc(logs)[i]) + # end + + if first + pop!(linesegs[]) + pop!(linecolors[]) + first = false + else + # # colors + # if !isnothing(lcolormapfunc) + # push!(linecolors[], lcolormapfunc(logs)[i]) + # end + end + end + pop!(linesegs[]) + pop!(linecolors[]) + push!(notes[], log.note) + end + + markercolors[] = markercolors[] + linecolors[] = linecolors[] + end + + Makie.Observables.onany( + update_plot, + tr2d.log, + tr2d.linecolormap, + tr2d.markercolormap, + lcolormapfunc, + tr2d.mcolormapfunc, + ) + + # init + update_plot( + tr2d.log[], + tr2d.linecolormap[], + tr2d.markercolormap[], + lcolormapfunc[], + tr2d.mcolormapfunc[], + ) + + linesegments!( + tr2d, + linesegs, + color = linecolors, + linewidth = tr2d.linewidth, + linestyle = tr2d.linestyle, + visible = tr2d.showline, + # inspector_label = (self, i, pos) -> + ) + scatter!( + tr2d, + points, + color = markercolors, + markersize = tr2d.markersize, + strokewidth = tr2d.strokewidth, + visible = tr2d.showmarker, + ) + # @info "dump" dump(tr2d, maxdepth = 1) + # @info "attributes" dump(tr2d.attributes, maxdepth = 3) + + tr2d +end diff --git a/src/visualize.jl b/src/visualize.jl index e36c076..1288422 100644 --- a/src/visualize.jl +++ b/src/visualize.jl @@ -10,7 +10,7 @@ function _plot_map!(ax, mappath::AbstractString) heatmap!( ax, (1:width) .- width ÷ 2, - (1:heigh) .- width ÷ 2, + (1:heigh) .- heigh ÷ 2, rotr90(map), inspectable = false, ) @@ -25,213 +25,3 @@ function view_with_map(log::CoordLog; map = "map.png") return fig end -""" -Predefined color map functions. - -# Interface - (map, AbstractVector{CoordLog}) -> Vector{<: Colorant} -""" -module ColorMapFunc - -using ..CoordVisualize: CoordLog, n_coords -using ColorTypes -using Dates: DateTime -using Makie: wong_colors, Scene - -"Use same color." -struct Constant - color::Colorant -end - -Constant(c::Symbol) = Constant(parse(Colorant, c)) - -function (c::Constant)(map, logs) - # Iterators.repeated(c.color, length(logs)) - fill(c.color, sum(n_coords, logs)) -end - -"Use colormap." -struct ColorMap - colormap::AbstractVector{<:Colorant} -end - -ColorMap() = ColorMap(wong_colors()) -ColorMap(cmap::Vector{Symbol}) = ColorMap(map(cmap) do s - parse(Colorant, s) -end) -function ColorMap(scene::Scene) - ColorMap(theme(scene, :linecolor)) -end - -function (cm::ColorMap)(map, logs) - cm = Iterators.cycle(cm.colormap) - nlog_s = Iterators.map(n_coords, logs) - Iterators.map(zip(cm, nlog_s)) do (color, count) - Iterators.repeated(color, count) - end |> Iterators.flatten |> collect -end - -"Color depending on log date." -function date(cmap, logs::AbstractVector{CoordLog}) - logdates::Vector{DateTime} = map(logs) do log - fill(log.logdate, n_coords(log)) - end |> (v -> vcat(v...)) - lst, fst = extrema(logdates) - normeddate = (logdates .- lst) ./ (fst - lst) - return get.(Ref(cmap), normeddate) -end - -"Color depending on altitude." -function altitude(cmap, logs::AbstractVector{CoordLog}) - altitudes = map(logs) do log - map(eachrow(log.coords)) do c - c[2] - end - end |> Iterators.flatten |> collect - low, high = extrema(altitudes) - normedalt = (altitudes .- low) ./ (high - low) - return get.(Ref(cmap), normedalt) -end - -end # module ColorMapFunc - -# TODO: alpha? -""" - trace2ds(log::Vector{CoordLog}) - -# Arguments -TODO -""" -@recipe(Trace2Ds, log) do scene - Attributes(; - marker = theme(scene, :marker), - markercolormap = theme(scene, :colormap), - markersize = theme(scene, :markersize), - strokewidth = theme(scene, :strokewidth), - linecolormap = theme(scene, :colormap), - linestyle = theme(scene, :linestyle), - linewidth = theme(scene, :linewidth), - inspectable = theme(scene, :inspectable), - lcolormapfunc = ColorMapFunc.ColorMap(), # or func like in ColorMapFunc - mcolormapfunc = ColorMapFunc.ColorMap(), - ) -end - -function Makie.plot!(tr2d::Trace2Ds) - # @info "logs" tr2d - # @info "fieldnames" tr2d.log - # @info "" theme(tr2d, :colormap) - - lcolormapfunc = tr2d.lcolormapfunc - - ntraces = length(tr2d.log[]) # number of CoordLog - linesegs = Observable(Point2f[]) - points = Observable(Point2f[]) - notes = Observable(String[]) - if tr2d.markercolormap[] isa Symbol - tr2d.markercolormap[] = getproperty(ColorSchemes, tr2d.markercolormap[]) - end - markercolors = Observable(tr2d.mcolormapfunc[](tr2d.markercolormap[], tr2d.log[])) - if tr2d.linecolormap[] isa Symbol - tr2d.linecolormap[] = getproperty(ColorSchemes, tr2d.linecolormap[]) - end - # @info "lcolormapfunc" lcolormapfunc - linecolors = Observable(lcolormapfunc[](tr2d.linecolormap[], tr2d.log[])) - - # helper function which mutates observables - function update_plot( - logs::AbstractVector{<:CoordLog}, - lcolormap, - mcolormap, - lcolormapfunc, #::Union{Symbol, Tuple{Symbol, Symbol}}, - mcolormapfunc, - ) - @info "update_plot" - markercolors[] - linecolors[] - # @info "logs on update_plot" logs - # init - empty!(linesegs[]) - empty!(points[]) - empty!(markercolors[]) - if linecolors[] isa AbstractVector - empty!(linecolors[]) - else - linecolors[] = [] - end - - # update - colors_count = 1 - for (i, log) in enumerate(logs) - first = true - for point in eachrow(log.coords) - push!(linesegs[], Point2f(point[1], point[3])) - push!(linesegs[], Point2f(point[1], point[3])) - push!(points[], Point2f(point[1], point[3])) - push!(linecolors[], lcolormapfunc(lcolormap, logs)[colors_count]) - push!(linecolors[], lcolormapfunc(lcolormap, logs)[colors_count]) - push!(markercolors[], mcolormapfunc(mcolormap, logs)[colors_count]) - colors_count += 1 - - # # marker - # if !isnothing(mcolormapfunc) - # push!(markercolors[], mcolormapfunc(logs)[i]) - # end - - if first - pop!(linesegs[]) - pop!(linecolors[]) - first = false - else - # # colors - # if !isnothing(lcolormapfunc) - # push!(linecolors[], lcolormapfunc(logs)[i]) - # end - end - end - pop!(linesegs[]) - pop!(linecolors[]) - push!(notes[], log.note) - end - - markercolors[] = markercolors[] - linecolors[] = linecolors[] - end - - Makie.Observables.onany( - update_plot, - tr2d.log, - tr2d.linecolormap, - tr2d.markercolormap, - lcolormapfunc, - tr2d.mcolormapfunc, - ) - - # init - update_plot( - tr2d.log[], - tr2d.linecolormap[], - tr2d.markercolormap[], - lcolormapfunc[], - tr2d.mcolormapfunc[], - ) - - linesegments!( - tr2d, - linesegs, - color = linecolors, - linewidth = tr2d.linewidth, - linestyle = tr2d.linestyle, - ) - scatter!( - tr2d, - points, - color = markercolors, - markersize = tr2d.markersize, - strokewidth = tr2d.strokewidth, - ) - # @info "dump" dump(tr2d, maxdepth = 1) - # @info "attributes" dump(tr2d.attributes, maxdepth = 3) - - tr2d -end