---
title: "Shiny workflows"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Shiny workflows}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
```

`dragmapr` is useful in Shiny when the draggable plot is the interface itself.
Users can move regions, panels, or labels directly instead of typing numeric
offsets.

## Embed A Draggable Plot

The simplest Shiny pattern is to write a helper HTML file into a Shiny resource
directory and show it in an iframe.

```{r, eval = FALSE}
if (interactive()) {
  shiny::runApp(system.file("examples", "shiny_draggable_plot.R", package = "dragmapr"))
}
```

This is useful when the app only needs to let users explore or compose a layout.

## Custom Labels

Apps can provide their own label table instead of using one label per region.
This is useful for annotations, review notes, callouts, or workflow-specific
metadata. Labels can be ordinary text labels, text-only draggable labels, or
annotation boxes created with `as_drag_annotations()`.

```{r, eval = FALSE}
if (interactive()) {
  shiny::runApp(system.file("examples", "shiny_custom_labels.R", package = "dragmapr"))
}
```

The user-supplied label table is created with `as_drag_labels()` and can carry
extra columns for app-specific behavior. Connector columns such as `connector`,
`connector_type`, `connector_start_x`, and `connector_mid_x` are also preserved,
so apps can let users choose straight, elbow, curved, or squiggle leader lines.

## Export A Report Image

Some apps need a static image after the user has finished dragging. The helper
posts offset state to its Shiny parent window. The Shiny app can then render a
preview and expose a PNG download.

```{r, eval = FALSE}
if (interactive()) {
  shiny::runApp(system.file("examples", "shiny_draggable_export.R", package = "dragmapr"))
}
```

This pattern is useful for report builders, document workflows, or review apps
where the interactive layout is the editing surface and the exported PNG is the
deliverable.

The bundled `shiny_draggable_export.R` app intentionally has many controls
because it acts as a smoke test for the package surface:

- show or hide labels;
- switch between short labels and info boxes;
- show text labels with or without circular markers;
- change info-box width and height;
- show connector lines;
- choose straight, elbow, curve, or squiggle connectors;
- adjust connector thickness, line pattern, and arrow endpoints;
- show or hide the region legend;
- filter visible legend keys and labels while preserving offsets;
- show origin outlines, movement connectors, and browser-only drag trails;
- download a static PNG reconstructed from the current region and label offset
  state.

The important design point is that the Shiny app does not hold magic layout
state. The parent app receives the same region and label offset tables that a
non-Shiny workflow can copy or download from the helper.

## Static Only

If offsets have already been saved, an app can skip the draggable helper and
only provide static preview/export.

```{r, eval = FALSE}
if (interactive()) {
  shiny::runApp(system.file("examples", "shiny_static_export.R", package = "dragmapr"))
}
```

## Spatial Studio

`shiny_spatial_studio.R` is the most complete Shiny example. It is a small
spatial workspace rather than a fixed demo: users upload local polygon data,
or reopen a saved project ZIP; pick grouping and label columns; edit label text
and colors; undo and redo drag-state changes; drag boundaries; switch between
short labels and info boxes; control text size, connector geometry, connector
line color, line pattern, smart connectors, arrow endpoints, legend title,
visible legend keys, visible labels, origin outlines, movement connectors, drag
preview trails, and map background; preview a static render; set static export
size and DPI; and download the current artifacts.

```{r, eval = FALSE}
if (interactive()) {
  shiny::runApp(system.file("examples", "shiny_spatial_studio.R", package = "dragmapr"))
}
```

Supported inputs: zipped shapefiles, shapefile sidecar files (`.shp` + `.dbf` +
`.shx`), GeoJSON, and GeoPackage files uploaded locally. The app reads geometry
with `sf::st_read()`, repairs it with `sf::st_make_valid()`, and transforms
longitude/latitude data to EPSG:3857 so metre offsets work correctly. Region
group names are sorted using a natural (numeric-aware) order, so "1", "2", ...,
"10" are displayed in the right sequence rather than lexicographic order.

The label sidebar consolidates controls so only sliders relevant to the current
label type are shown: text size is always visible; marker size (width + height)
appears only for rounded-box labels; circle radius only for circle labels; box
dimensions only for info boxes.

Available downloads from the studio sidebar:

- **PNG** — static render from `render_dragged_map()`
- **Region CSV** — current region offsets, importable by any later session
- **Label CSV** — current label offsets
- **Labels table** — current label geometry and edited label text
- **GeoJSON / GPKG** — the adjusted sf geometry with offsets applied
- **HTML helper** — the standalone D3 drag file you can share or reopen
- **Project ZIP** — source geometry, offsets, labels, `palette.csv`,
  `metadata.json`, and `recreate-static-map.R`; enough to reopen the project in
  Spatial Studio, reconstruct the layout in a new session, including legend and
  label selections plus movement context settings, or hand off to a collaborator
- **Static bundle** — the project bundle plus ready-to-share PNG and PDF files

Spatial Studio intentionally keeps its adjusted-geometry exports focused on
GeoJSON and GeoPackage. If another shape format is needed, download either
file, open it in [Mapshaper](https://mapshaper.org/), and export the required
format there. The same handoff works for an `sf` object created elsewhere in R:
write it to a supported spatial file, open that file in Mapshaper, and choose
the desired export format.

The export panel also includes a reproducible R script and a static bundle. The
R script calls `render_dragmapr_project()` and expects `dragmapr-project.zip` to
be in the same folder unless `project_path` is edited. The static bundle
includes the project files plus ready-to-share PNG and PDF files. The project
ZIP is a one-line static rendering input:

```{r, eval = FALSE}
render_dragmapr_project(
  "dragmapr-project.zip",
  file = "final-map.png",
  width = 10,
  height = 8,
  dpi = 300
)
```

`render_dragmapr_project()` validates the bundle before rendering. It gives
file-specific errors for missing metadata, malformed CSVs, unknown region
columns, and labels that refer to regions that are not present in `source.gpkg`.
When an offset row is absent, it reports the missing rows and uses zero movement
for that region or label so users can still inspect the output.

When launched with `?debug=1`, the app includes a State tab that names its
central reactive values so they are easy to find when adapting the code:

- `source_sf()` - raw sf from upload / demo / project bundle
- `projected_sf()` — after `prepare_dragmapr_sf()`
- `region_col()`, `label_col()` — chosen columns
- `label_table()` — styled label data frame passed to `drag_map_prototype()`
- `region_state()`, `label_state()` — current drag offsets as data frames
- `region_palette()` — named colour vector
- `current_plot()` — `ggplot2` object ready to save

### Loading veil

The studio shows a loading veil while data is being read and the D3 helper is
being built. The veil is dismissed when the helper iframe signals it has
finished its first `render()` call by posting a `dragmapr-ready` message to the
parent page. This avoids the race condition where the browser fires the iframe
`load` event before the parent's listener has been attached.

## Reusable Shiny Helpers

The spatial studio internally uses four package-level helpers that are also
available directly for custom apps.

**`read_dragmapr_sf_upload(upload)`** wraps a [shiny::fileInput()] result -
including multi-file shapefile sidecar uploads and zip archives - into a single
`sf::st_read()` call.  Returns `NULL` when the upload is empty so callers can
fall back to demo data.

**`read_dragmapr_sf_url(url, timeout = 60)`** downloads a spatial file from a
URL into a temporary directory, unpacks zip archives, and returns an `sf`
object.  Raises a descriptive error on network or format failures.

**`prepare_dragmapr_sf(x, target_crs = 3857)`** repairs invalid geometry, keeps
only polygon types, assigns a fallback CRS when none is present, and reprojects
geographic data to the target projected CRS so metre offsets are meaningful.

**`dragmapr_iframe_bridge(...)`** returns a JavaScript string that installs the
`postMessage` listener and polling loop needed to relay drag state from the
helper iframe back to Shiny inputs.  Wrap it in `tags$head(tags$script(HTML(
dragmapr_iframe_bridge())))` in your UI.  The function accepts `region_input`,
`label_input`, `slow_poll_ms`, `fast_poll_ms`, `allowed_origin`, and
`iframe_selector` arguments to customise the input names, timing, origin check,
and helper iframe selection.

```{r, eval = FALSE}
library(shiny)
library(dragmapr)

ui <- fluidPage(
  tags$head(tags$script(HTML(dragmapr_iframe_bridge()))),
  uiOutput("helper")
)

server <- function(input, output, session) {
  helper_dir <- tempfile("myapp_")
  dir.create(helper_dir)
  shiny::addResourcePath("myapp_static", helper_dir)

  x <- prepare_dragmapr_sf(my_sf)
  drag_map_prototype(x, region_col = "region",
                     file = file.path(helper_dir, "helper.html"))

  output$helper <- renderUI(
    tags$iframe(src = "myapp_static/helper.html",
                style = "width:100%;height:700px;border:none;")

## Switching Grouping Columns in Spatial Studio

Spatial Studio stores drag positions per region column and propagates them
when you change the **Group / region column** dropdown. This means you can
work at multiple levels of geographic hierarchy without losing your layout.

### How inheritance works

Each column maintains its **own independent layout cache**. Switching columns
never displaces regions that you have not personally dragged in that column:

- **Coarser → finer** (e.g. HHS region → state name): the finer column resumes
  its own last saved layout. If you have not visited it before, all fine units
  start at their natural geographic positions — they are **not** displaced by
  the parent column's drag offsets.

- **Finer → coarser** (e.g. state name → HHS region): each parent group is
  placed at the **mean** of its member units' current positions, or (if you
  choose "Restore parent's last position") at the position the parent had when
  you last worked at that column.

### Example: HHS regions and state names

The bundled HHS demo has both an `hhs_region` column (ten groups) and a `NAME`
column (individual states). A typical workflow:

1. Set **Group column** to `hhs_region`. Drag the ten regions into an exploded
   layout.
2. Switch to `NAME`. The states appear at their **natural positions** (first
   visit) — ready for individual fine-tuning without any carry-over from the
   HHS drag.
3. Fine-tune individual states as needed.
4. Switch back to `hhs_region`. Each region lands at the mean of its states'
   current positions, reflecting any individual fine-tuning.

### What resets and what is preserved

| On column switch | Behaviour |
|---|---|
| Region offsets (coarser→finer) | Restored to that column's last saved positions, or zero if never visited |
| Region offsets (finer→coarser) | Average of children's positions, or restored to parent's last position |
| Label offsets | Reset — label IDs are derived from the new column's region names |
| Undo / redo stack | Reset — new column starts with a clean history |
| Region palette | Preserved |
| Legend and label filter selections | Preserved |

### Round-trip precision

The only step that involves averaging is **finer → coarser**. If all child
regions had identical offsets, the round-trip is lossless. Mixed individual child
moves are summarised into an average for the parent — or you can choose
**"Restore parent's last position"** to skip averaging and return the parent to
exactly where it was.

Changing only the **Label column** while keeping the region column the same
leaves region offsets completely untouched. Only the label IDs change.

