diff --git a/.JuliaFormatter.toml b/.JuliaFormatter.toml new file mode 100644 index 0000000..df4710a --- /dev/null +++ b/.JuliaFormatter.toml @@ -0,0 +1,13 @@ +# See https://domluna.github.io/JuliaFormatter.jl/stable/ for a list of options +whitespace_ops_in_indices = true +remove_extra_newlines = true +always_for_in = true +whitespace_typedefs = true +normalize_line_endings = "unix" +# format_docstrings = true +# format_markdown = true +align_assignment = true +align_struct_field = true +align_conditional = true +align_pair_arrow = true +align_matrix = true diff --git a/Project.toml b/Project.toml index 3150d03..fbaed4b 100644 --- a/Project.toml +++ b/Project.toml @@ -1,12 +1,19 @@ name = "CoordVisualize" uuid = "4c41ebcf-33aa-4478-9aac-83d12758d145" authors = ["qwjyh "] -version = "0.1.0" +version = "1.0.0" [deps] ColorSchemes = "35d6a980-a343-548e-a6ea-1d62b119f2f4" +ColorTypes = "3da002f7-5984-5a60-b8a6-cbb66c0b333f" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" GLMakie = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a" +Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" +MakieCore = "20f20a25-4f0e-4fdf-b5d1-57303727442b" +Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" +Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" YAML = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6" [extras] diff --git a/README.adoc b/README.adoc index d0772bd..fdf3658 100644 --- a/README.adoc +++ b/README.adoc @@ -13,8 +13,24 @@ * Visualize with GLMakie (or CairoMakie) ** Inspecting with GUI +== Docs +Clone this repo, and +```sh +$ cd docs + +$ julia --project -e 'using Pkg; Pkg.instantiate()' + +$ julia --project make.jl + +$ cd build + +$ python -m http.server --bind localhost + +``` + == TODO -- [ ] Printing +- [x] Printing - [ ] visualize +- [ ] interactive edit - [ ] doc diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..a303fff --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,2 @@ +build/ +site/ diff --git a/docs/Project.toml b/docs/Project.toml new file mode 100644 index 0000000..c36628f --- /dev/null +++ b/docs/Project.toml @@ -0,0 +1,3 @@ +[deps] +CoordVisualize = "4c41ebcf-33aa-4478-9aac-83d12758d145" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" diff --git a/docs/make.jl b/docs/make.jl new file mode 100644 index 0000000..4371c07 --- /dev/null +++ b/docs/make.jl @@ -0,0 +1,19 @@ +using Documenter +using CoordVisualize + +makedocs( + sitename = "CoordVisualize", + format = Documenter.HTML(), + modules = [CoordVisualize], + pages = [ + "Top" => "index.md", + "API list" => "apis.md" + ] +) + +# Documenter can also automatically deploy documentation to gh-pages. +# See "Hosting Documentation" and deploydocs() in the Documenter manual +# for more information. +#=deploydocs( + repo = "" +)=# diff --git a/docs/src/apis.md b/docs/src/apis.md new file mode 100644 index 0000000..6f203de --- /dev/null +++ b/docs/src/apis.md @@ -0,0 +1,14 @@ +# API list + +```@index +``` + +```@autodocs +Modules = [CoordVisualize] +``` + +# ColorMapFuncs + +```@autodocs +Modules = [CoordVisualize.ColorMapFuncs] +``` diff --git a/docs/src/index.md b/docs/src/index.md new file mode 100644 index 0000000..293bc14 --- /dev/null +++ b/docs/src/index.md @@ -0,0 +1,95 @@ +```@meta +CurrentModule = CoordVisualize +``` + +# CoordVisualize.jl + +Documentation for CoordVisualize.jl + +## Tutorial +Readers are expected to be familiar with basics of julia. + +### Preparing +This will take a few minutes. + +```julia-repl +julia> # type ] + +(@v1.10) Pkg> activate . + +(CoordVisualize) Pkg> instantiate +``` + +### Parse log +```julia-repl +julia> using CoordVisualize + +julia> iedit_log("coord_log_1.txt", "coord_log_2.txt") +... + Follow the instruction +... +``` + +### Visualize the log +Get map image file and place it as "map.png" beforehand. + +```julia-repl +julia> using GLMakie, CoordVisualize + +julia> tlog = Observable(include("")) +... + +julia> # or + +julia> tlog = Observable(interactive_edit_log("log files", "log file2")) +... + +julia> include("/interactive_viz.jl") +... +``` + +Available colorschemes at https://juliagraphics.github.io/ColorSchemes.jl/stable/catalogue/ . +Available colors at https://juliagraphics.github.io/Colors.jl/stable/constructionandconversion/#Color-Parsing and https://juliagraphics.github.io/Colors.jl/stable/namedcolors/ . + +### Edit the log +```julia-repl +julia> isplit_log!(tlog[], 3, 30) +... + +julia> iedit_note!(tlog[], 3) +... + +julia> ijoin_logs!(tlog[], 5, 7) +... + +``` + +### Export the log +```julia-repl +julia> export_log(tlog[], "") +``` + +## Low level + +### Log structure +CoordVisualize.jl treats coordination trace log with some additional information, +datetime when log was taken and supplemental note to annotate the log. +This set of log is represented by the type [`CoordLog`](@ref). + +### Parsing Log +Use [`parse_log`](@ref) to parse log files generated with Tracecoords CSM mod. +Set the keyword argument `interactive` to `true` to supply notes interactively. +It automatically get datetime. +Notes can be also supplied in the following section. + +### Editing +You sometimes want to split logs and to give more appropriate notes for each of them. +You can do this with [`split_log`](@ref) function. + +You can also edit existing notes with [`assign_note!`](@ref). + +### Exporting +Use [`export_log`](@ref) to export log to `io` or `file`. + +### Importing +Do `using Dates` first and just `include("filename")` and it will return `Vector{CoordLog}`. diff --git a/interactive_viz.jl b/interactive_viz.jl new file mode 100644 index 0000000..18e7c9c --- /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, 3] = 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, 2] = 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 e0a8927..5726392 100644 --- a/src/CoordVisualize.jl +++ b/src/CoordVisualize.jl @@ -2,8 +2,18 @@ module CoordVisualize using Dates +export CoordLog +export iedit_log +export isplit_log!, iedit_note!, ijoin_logs! +export export_log +export ColorMapFuncs + include("typedef.jl") include("parser.jl") include("edit.jl") +include("interactive_edit.jl") +include("print.jl") +include("recipes.jl") +include("visualize.jl") end # module CoordVisualize diff --git a/src/edit.jl b/src/edit.jl index 3458d34..7a432dd 100644 --- a/src/edit.jl +++ b/src/edit.jl @@ -3,7 +3,12 @@ Split `log` at `at`, i.e. to `1:at` and `(at + 1):end` then assign `notes_1` and `notes_2` to notes for each other. """ -function split_log(log::CoordLog, at::Unsigned, notes_1::AbstractString, notes_2::AbstractString)::Tuple{CoordLog, CoordLog} +function split_log( + log::CoordLog, + at::Unsigned, + notes_1::AbstractString, + notes_2::AbstractString, +)::Tuple{CoordLog, CoordLog} @assert at < size(log.coords)[1] "Split index must be less than original log length($(size(log.coords)[1]))" ( CoordLog(log.coords[1:at, :], log.logdate, notes_1), @@ -11,11 +16,15 @@ function split_log(log::CoordLog, at::Unsigned, notes_1::AbstractString, notes_2 ) end -function split_log(log::CoordLog, at::Integer, notes_1::AbstractString, notes_2::AbstractString)::Tuple{CoordLog, CoordLog} +function split_log( + log::CoordLog, + at::Integer, + notes_1::AbstractString, + notes_2::AbstractString, +)::Tuple{CoordLog, CoordLog} split_log(log, UInt(at), notes_1, notes_2) end - """ assign_note!(log::CoordLog, new_note::AbstractString) @@ -24,3 +33,21 @@ Replace `note` in `log` with `new_note`. function assign_note!(log::CoordLog, new_note::AbstractString) log.note = new_note end + +""" + join_log( + log1::CoordLog{T}, + log2::CoordLog{T}, + note::AbstractString, + )::CoordLog{T} where {T} + +Join two logs. +""" +function join_log( + log1::CoordLog{T}, + log2::CoordLog{T}, + note::AbstractString, +)::CoordLog{T} where {T} + newdate = min(log1.logdate, log2.logdate) + CoordLog(vcat(log1.coords, log2.coords), newdate, note) +end diff --git a/src/interactive_edit.jl b/src/interactive_edit.jl new file mode 100644 index 0000000..b92fb50 --- /dev/null +++ b/src/interactive_edit.jl @@ -0,0 +1,193 @@ +using Statistics +using Dates +using Printf + +""" +Interactively parse log files and edit the log. +""" +function iedit_log(filenames...; writetofile = true) + printstyled(stdout, "[CoordLog Editor] \n", color = :blue, bold = true) + logs = CoordLog[] + printstyled(stdout, "loading log files\n", color = :blue) + for file in filenames + append!(logs, parse_log(file)) + end + printstyled(stdout, "all files loaded\n", color = :blue) + edited_logs = CoordLog{Float64}[] + for (i, log) in enumerate(logs) + printstyled(stdout, "LogEdit: editing log $(i) / $(length(logs))\n", color = :blue) + printstyled(stdout, "summary\n", color = :cyan) + println( + stdout, + """ + mean : $(mean(eachrow(log.coords)) .|> round |> Tuple) + start : $(log.coords[1, :] .|> round |> Tuple) + end : $(log.coords[end, :] .|> round |> Tuple) + datetime : $(Dates.format(log.logdate, DateFormat("yyyy-mm-dd HH:MM:SS"))) + number of coords: $(size(log.coords)[1]) + """, + ) + @label ask + printstyled(stdout, "split log?(y/N): ", color = :green, italic = true) + ans = readline(stdin) + if ans == "y" || ans == "Y" + while true + maximum = size(log.coords)[1] + printstyled( + stdout, + "split at where (1 to n, n to end), max = $(maximum): ", + color = :green, + italic = true, + ) + at = try + parse(UInt64, readline(stdin)) + catch + printstyled("invalid input, please type number\n", color = :red) + continue + end + if at ≥ maximum + printstyled("too large number; max = $(maximum)\n", color = :red) + continue + end + if at == 0 + printstyled("must be larger than 0\n", color = :red) + continue + end + print(""" + summary of the first log: + mean : $(mean(eachrow(log.coords)[1:at]) .|> round |> Tuple) + start : $(log.coords[1, :] .|> round |> Tuple) + end : $(log.coords[at, :] .|> round |> Tuple) + """) + printstyled("note for the first log: ", color = :green, italic = true) + note_1 = readline(stdin) + new_log, log = split_log(log, at, note_1, "") + push!(edited_logs, new_log) + print( + """ + summary of the remaining log: + mean : $(mean(eachrow(log.coords)) .|> round |> Tuple) + start : $(log.coords[1, :] .|> round |> Tuple) + end : $(log.coords[end, :] .|> round |> Tuple) + datetime : $(Dates.format(log.logdate, DateFormat("yyyy-mm-dd HH:MM:SS"))) + number of coords: $(size(log.coords)[1]) + """, + ) + @goto ask + end + elseif ans == "n" || ans == "N" || ans == "" + printstyled("note for the log: ", color = :green, italic = true) + note = readline() + assign_note!(log, note) + push!(edited_logs, log) + else + printstyled("invalid ans; type y or n\n", color = :red) + @goto ask + end + end + println() + printstyled("Finish editing\n", color = :blue, bold = true) + printstyled("number of logs: $(length(edited_logs))\n", color = :cyan) + printstyled("summary: length, note\n", color = :cyan) + len_ncoords = maximum(ndigits.(n_coords.(edited_logs))) + for log in edited_logs + println(" ", lpad(n_coords(log), len_ncoords), " ", log.note) + end + if writetofile + printstyled("Writing to file\n", color = :blue, bold = true) + printstyled("filename: ", color = :green, italic = true) + filename = readline() + if filename in readdir() + printstyled("$(filename) already exists.", color = :magenta) + printstyled("Are you sure to overwrite? (y/N)", color = :magenta, italic = true) + ans = readline() + if ans == "y" || ans == "Y" + elseif ans == "n" || ans == "N" || ans == "" + printstyled( + "Skip exporting to a file. Please export the returned log manually.\n", + color = :magenta, + ) + @goto finish + end + end + open(filename, "w") do f + println(f, "using Dates") + println(f, export_log(edited_logs)) + end + printstyled("Exported log to the file: $(filename)\n", color = :blue) + end + @label finish + printstyled("Edit completed.\n", color = :blue, bold = true) + return edited_logs +end + +""" + isplit_log!( + logs::AbstractVector{CoordLog{T}}, + logid::Integer, + pointid::Integer, + ) where {T} + +Split the log. Supply notes interactively. +""" +function isplit_log!( + logs::AbstractVector{CoordLog{T}}, + logid::Integer, + pointid::Integer, +) where {T} + 1 ≤ logid ≤ length(logs) || + throw(ArgumentError("logid out of index: ¬ 1 ≤ $(logid) ≤ $(length(logid))")) + if !(1 < pointid < n_coords(logs[logid])) + throw( + ArgumentError( + "pointid($(pointid)) out of index: min=2, max=$(n_coords(logs[logid]) - 1)", + ), + ) + end + + log = popat!(logs, logid) + printstyled("note for the first log: ", color = :green, italic = true) + note_1 = readline() + printstyled("note for the second log: ", color = :green, italic = true) + note_2 = readline() + new_logs = split_log(log, pointid, note_1, note_2) + insert!(logs, logid, new_logs[1]) + insert!(logs, logid + 1, new_logs[2]) +end + +""" + iedit_note!(logs::AbstractVector{CoordLog{T}}, logid::Integer) where {T} + +Edit the note at `logid`. Supply new note interactively. +""" +function iedit_note!(logs::AbstractVector{CoordLog{T}}, logid::Integer) where {T} + 1 ≤ logid ≤ length(logs) || + throw(ArgumentError("logid out of index: ¬ 1 ≤ $(logid) ≤ $(length(logid))")) + printstyled("new note for the log: ", color = :green, italic = true) + note = readline() + logs[logid].note = note +end + +""" + ijoin_logs!(logs::AbstractVector{CoordLog{T}}, logid1::Integer, logid2::Integer) where {T} + +Join the logs at `logid1` and `logid2`. Supply new note interactively. +""" +function ijoin_logs!(logs::AbstractVector{CoordLog{T}}, logid1::Integer, logid2::Integer) where {T} + 1 ≤ logid1 ≤ length(logs) || + throw(ArgumentError("logid1 out of index: ¬ 1 ≤ $(logid1) ≤ $(length(logid1))")) + 1 ≤ logid2 ≤ length(logs) || + throw(ArgumentError("logid2 out of index: ¬ 1 ≤ $(logid2) ≤ $(length(logid2))")) + logid1 == logid2 && throw(ArgumentError("logid1 and logid2 cannot be the same")) + if logid1 > logid2 + logid1, logid2 = logid2, logid1 + end + + log_1 = popat!(logs, logid1) + log_2 = popat!(logs, logid2 - 1) + + printstyled("note for the new log: ", color = :green, italic = true) + note = readline() + log = join_log(log_1, log_2, note) + insert!(logs, logid1, log) +end 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/print.jl b/src/print.jl new file mode 100644 index 0000000..035f76c --- /dev/null +++ b/src/print.jl @@ -0,0 +1,43 @@ +""" +Export `log` to a file or `io::IO`. +""" +function export_log end + +function export_log(log::CoordLog) + """ + CoordLog( + $(log.coords), + Dates.DateTime("$(log.logdate)"), "$(log.note)" + )""" +end + +function export_log(logs::Vector{CoordLog{T}}) where {T} + logs .|> + export_log |> + (vs -> join(vs, ",\n")) |> + (s -> "[\n" * s * "\n]") +end + +function export_log(logs::Vector{CoordLog{T}}, filename::AbstractString) where {T} + open(filename, "w") do f + println(f, "using Dates") + println(f, export_log(logs)) + end +end + +""" + export_log(io::IO, log) +""" +function export_log(io::IO, log) + write(io, export_log(log)) +end + +""" + export_log(file::AbstractString, log) +""" +function export_log(file::AbstractString, log) + open(file, "w") do f + export_log(f, log) + end +end + diff --git a/src/recipes.jl b/src/recipes.jl new file mode 100644 index 0000000..76c87a1 --- /dev/null +++ b/src/recipes.jl @@ -0,0 +1,291 @@ +using GLMakie +using ColorTypes +using ColorSchemes + +""" +Predefined color map functions. +Receives +- `cmap`: colormap +- `logs`: vector of `CoordLog` +- `n`: number of returning ticks + +and returns tuple of +1. vector of `Colorant` +2. ticks to pass to `Colorbar`, which is a Tuple of + 1. vector of tick location (0 to 1) + 2. vector of tick labels (strings) + +Any function (or struct) which behaves like this can be used for +`lcolormapfunc` and `mcolormapfunc` kwargs of `trace2ds`. + +# Types + +[`ColorMapFunc`](@ref) is a supertype of all of these. + +# Interface + +Define these methods for the ColorMapFunc. + + (cmap, logs, n) -> Vector{Colorant}, 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 struct for those use vector of 0 to 1 floats. +Example functions are [`Date`](@ref) and [`Altitude`](@ref). +""" +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[]) + point_ids = Observable(Tuple{Int64, Int64}[]) + 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!(point_ids[]) + 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 (j, point) in enumerate(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!(point_ids[], (i, j)) + 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, + inspector_label = (self, i, pos) -> begin + logid, pointid = point_ids[][i] + """ + log: $(logid), point: $(pointid) + x: $(lpad(round(pos[1], digits = 1), 7)) + y: $(lpad(round(altitudes[][i], digits = 1), 7)) + z: $(lpad(round(pos[2], digits = 1), 7)) + $(tr2d.log[][logid].note) + """ + end, + ) + # @info "dump" dump(tr2d, maxdepth = 1) + # @info "attributes" dump(tr2d.attributes, maxdepth = 3) + + tr2d +end diff --git a/src/typedef.jl b/src/typedef.jl index 7f722ba..8ee6182 100644 --- a/src/typedef.jl +++ b/src/typedef.jl @@ -1,4 +1,8 @@ using Dates +import Base +""" +Stores a set of logs with its taken date datetime and supplemental note. +""" mutable struct CoordLog{T <: AbstractFloat} coords::Matrix{T} logdate::DateTime @@ -13,3 +17,11 @@ Get number of coordinates in `log`. function n_coords(log::CoordLog)::Integer size(log.coords)[1] end + +Base.:(==)(x::CoordLog, y::CoordLog) = begin + x.note == y.note && x.logdate == y.logdate && x.coords == y.coords +end + +function Base.getindex(log::CoordLog{T}, i) where {T} + log.coords[i, :] +end diff --git a/src/visualize.jl b/src/visualize.jl new file mode 100644 index 0000000..1288422 --- /dev/null +++ b/src/visualize.jl @@ -0,0 +1,27 @@ +using GLMakie +using ColorTypes +using ColorSchemes +using FileIO + +function _plot_map!(ax, mappath::AbstractString) + map = load(mappath) + let + heigh, width = size(map) + heatmap!( + ax, + (1:width) .- width ÷ 2, + (1:heigh) .- heigh ÷ 2, + rotr90(map), + inspectable = false, + ) + end +end + +function view_with_map(log::CoordLog; map = "map.png") + fig = Figure() + ax = Axis(fig[1, 1], aspect = AxisAspect(1)) + _plot_map!(ax, map) + + return fig +end + diff --git a/test/runtests.jl b/test/runtests.jl index 3639d3b..1e42e9a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -5,11 +5,15 @@ using Test @testset "CoordVisualize" begin "Must be same as the first log in `sample_log.txt`" sample_log_1 = CoordVisualize.CoordLog[CoordVisualize.CoordLog{Float64}([-54.0 -10.000000953674 -35.000003814697; -54.0 -10.000000953674 -35.000003814697; -54.0 -10.000000953674 -36.013381958008; -54.0 -10.000000953674 -37.615753173828; -54.0 -10.000000953674 -39.261665344238; -54.0 -10.000000953674 -40.727695465088; -54.0 -10.000000953674 -42.168701171875; -54.0 -10.000000953674 -43.820377349854; -54.0 -11.018865585327 -47.018901824951; -54.0 -14.0 -51.176284790039; -54.663269042969 -14.0 -55.0; -58.297706604004 -14.0 -55.0; -63.16588973999 -16.0 -55.0; -66.000007629395 -16.0 -55.526763916016; -66.000007629395 -16.0 -59.460041046143; -66.000007629395 -16.0 -63.24658203125; -66.000007629395 -16.0 -67.261924743652; -66.000007629395 -16.0 -71.199310302734], Dates.DateTime("2023-10-22T10:02:04"), "")] + sample_log_1_2 = CoordVisualize.CoordLog[CoordVisualize.CoordLog{Float64}([-54.0 -10.000000953674 -35.000003814697; -54.0 -10.000000953674 -35.000003814697; -54.0 -10.000000953674 -36.013381958008; -54.0 -10.000000953674 -37.615753173828; -54.0 -10.000000953674 -39.261665344238; -54.0 -10.000000953674 -40.727695465088; -54.0 -10.000000953674 -42.168701171875; -54.0 -10.000000953674 -43.820377349854; -54.0 -11.018865585327 -47.018901824951; -54.0 -14.0 -51.176284790039; -54.663269042969 -14.0 -55.0; -58.297706604004 -14.0 -55.0; -63.16588973999 -16.0 -55.0; -66.000007629395 -16.0 -55.526763916016; -66.000007629395 -16.0 -59.460041046143; -66.000007629395 -16.0 -63.24658203125; -66.000007629395 -16.0 -67.261924743652; -66.000007629395 -16.0 -71.199310302734], Dates.DateTime("2023-10-22T10:02:04"), "")] "Must be same as the second log in `sample_log.txt`" sample_log_2 = CoordVisualize.CoordLog{Float64}([895.0 7.0 -978.0; 895.0 7.0 -978.0; 895.0 7.0 -977.38684082031; 895.0 7.0 -975.71923828125; 897.0 7.0 -974.39855957031; 898.80633544922 7.0 -973.0; 901.38275146484 7.0 -973.0; 904.18518066406 7.0 -973.0; 907.25793457031 7.0 -973.0; 911.19061279297 7.0 -973.0; 915.05682373047 7.0 -973.0; 919.1259765625 7.0 -973.0; 923.12609863281 7.0 -973.0; 926.94378662109 7.0 -973.0; 930.82952880859 7.0 -973.0; 934.84539794922 7.0 -973.0; 938.83020019531 7.0 -973.0; 944.04681396484 8.0 -973.0; 948.01483154297 8.0148372650146 -973.0; 951.48193359375 9.0000009536743 -973.0; 955.5927734375 10.000000953674 -973.0; 954.96008300781 10.000000953674 -973.0; 958.39764404297 11.000000953674 -973.0; 962.41009521484 12.000000953674 -973.0; 966.17108154297 12.000000953674 -973.0; 969.40936279297 12.000000953674 -973.0; 969.47576904297 13.0 -973.0; 973.32684326172 13.0 -973.0; 977.21990966797 13.0 -973.0; 981.09814453125 13.0 -973.0; 985.05871582031 13.0 -973.0; 989.03479003906 13.0 -973.0; 992.83026123047 13.0 -973.0; 996.90203857422 13.0 -973.0], DateTime("0000-01-01T00:00:00"), "") sample_result = CoordVisualize.parse_log("sample_log.txt"; interactive=false) + @testset "equality" begin + @test sample_log_1 == sample_log_1_2 + end @testset "parse" begin @debug sample_result @testset "parse with datetime" begin @@ -49,4 +53,9 @@ using Test @test splitted_2.note == "latter one" end end + + @testset "export" begin + re_evaled_log = CoordVisualize.export_log(sample_log_1) |> Meta.parse |> eval + @test re_evaled_log[1] == sample_log_1[1] + end end