Compare commits

...

10 commits

Author SHA1 Message Date
efc37be087 bump version 2024-01-07 19:03:29 +09:00
69a47a24e8 update doc 2024-01-07 18:58:29 +09:00
49fa679d9a interactive edits 2024-01-07 18:28:38 +09:00
e5db3cd694 new: interactive edit 2024-01-07 01:49:50 +09:00
d99771045e 2d visualization 2024-01-06 23:46:51 +09:00
9996227460 recipe Trace2d 2024-01-05 23:06:58 +09:00
3a6d0da83e (WIP) partial recipe for Trace2d 2024-01-05 22:03:21 +09:00
ecd0d1c7b7 update to 1.10 & add doc for print, importing 2024-01-02 08:33:46 +09:00
f03b5f25f3 add export 2023-11-14 00:28:22 +09:00
bcd54dc673
adding doc 2023-11-12 09:57:28 +09:00
18 changed files with 993 additions and 7 deletions

13
.JuliaFormatter.toml Normal file
View file

@ -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

View file

@ -1,12 +1,19 @@
name = "CoordVisualize"
uuid = "4c41ebcf-33aa-4478-9aac-83d12758d145"
authors = ["qwjyh <urataw421@gmail.com>"]
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]

View file

@ -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

2
docs/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
build/
site/

3
docs/Project.toml Normal file
View file

@ -0,0 +1,3 @@
[deps]
CoordVisualize = "4c41ebcf-33aa-4478-9aac-83d12758d145"
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"

19
docs/make.jl Normal file
View file

@ -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 = "<repository url>"
)=#

14
docs/src/apis.md Normal file
View file

@ -0,0 +1,14 @@
# API list
```@index
```
```@autodocs
Modules = [CoordVisualize]
```
# ColorMapFuncs
```@autodocs
Modules = [CoordVisualize.ColorMapFuncs]
```

95
docs/src/index.md Normal file
View file

@ -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("<exported log file>"))
...
julia> # or
julia> tlog = Observable(interactive_edit_log("log files", "log file2"))
...
julia> include("<path to root>/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)
... <with some prompts>
julia> iedit_note!(tlog[], 3)
... <with some promots>
julia> ijoin_logs!(tlog[], 5, 7)
... <with some prompts>
```
### Export the log
```julia-repl
julia> export_log(tlog[], "<filename>")
```
## 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}`.

205
interactive_viz.jl Normal file
View file

@ -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)

View file

@ -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

View file

@ -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

193
src/interactive_edit.jl Normal file
View file

@ -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

View file

@ -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

43
src/print.jl Normal file
View file

@ -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

291
src/recipes.jl Normal file
View file

@ -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

View file

@ -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

27
src/visualize.jl Normal file
View file

@ -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

View file

@ -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