Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions docs/user_guide/how-tos/story-maps.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
(how-to-story-map)=

# Create and Edit Story Maps

<video controls width="700">
<source src="https://github.com/user-attachments/assets/0bfac878-9915-4c04-82c0-361783f96628" type="video/mp4">
Your browser does not support the video tag.
</video>

Story maps let you present a sequence of map views (segments) with optional text and images. This guide explains how to create a story, edit segments, use the Story Editor, and preview the story.

## Creating a story

A story is created when you add the first **Story Segment** layer:

1. Open the **+** menu from the toolbar.
2. Add **Story Segment**. The new layer captures the current map view (zoom and extent) and becomes the first segment of a new story map.
3. The right panel will show the **Story Editor** so you can set story-level options and add more segments.

If no story exists yet, the Story Editor panel shows an **Add Story Segment** button that does the same thing as the toolbar option.

## Story Editor

With the **Story Editor** tab selected in the right panel, you can:

- **Edit story-level properties**: Title, Story Type (guided or unguided), Presentation Background Color, and Presentation Text Color.

:::{admonition} Presentation Colors
:class: attention
The presentation colors are used only in the Specta view.
:::

:::{admonition} Story Types
:class: tip

- **Guided** stories advance with previous/next controls;
- **unguided** stories follow the selected segment in the layer list, so the viewer can jump to any segment by selecting it.
:::

- **Add segments**: Click **Add Story Segment** at the bottom of the panel. Each new segment again captures the current map view and is appended to the story. You can then select it in the layer list and edit its properties (see below).

The Story Editor does not list or reorder segments directly; segment order follows the order of Story Segment layers in the **Segments** tab of the left panel.

## Adding segments

- **From the Story Editor**: Click **Add Story Segment** in the right panel. Position the map as desired before clicking; the new segment will use the current view.
- **From the Add Layer menu**: Add **Story Segment** as when creating the first segment. The new segment is added to the current story and uses the current map view.

After adding a segment, select it in the **Segments** tab to edit its properties in the **Object Properties** panel.

## Editing segment properties

Select a Story Segment layer in the left panel (under the **Segments** tab). The **Object Properties** panel shows that segment’s form:

- **Extent**: Use **Set Story Segment Extent** to snap the segment’s view to the current map extent and zoom. Helpful after panning/zooming to the area you want for that slide.
- **Segment Content**: Title, optional image URL, and markdown text for the narrative shown when that segment is active.
- **Transition**: Animation style and duration (in seconds) when moving to this segment.
- **Immediate** jumps there with no animation.
- **Linear** animates directly to the segment’s view.
- **Smooth** zooms out, pans to the segment, then zooms back in.
- **Symbology Override**: Optional overrides for other layers when this segment is active (e.g. visibility, opacity, or opening the symbology dialog for a target layer to set style). Add an override by choosing a target layer and configuring the options.

Changes are saved as you edit; no separate “Save” step is required.

## Preview toggle

At the top of the Story panel (right panel, when the Story Editor tab is active) there is a **Preview Mode** switch.

- **Preview Mode off** (default): The panel shows the **Story Editor** (story-level form and **Add Story Segment**). Use this to create and edit the story and segment properties.
- **Preview Mode on**: The panel shows the **Story Map** viewer: the same step-through experience as in full presentation mode (previous/next, segment content, map updates), but still inside the main JupyterGIS window.

Use Preview Mode to check how the story will look and behave without entering full-screen presentation. The switch is hidden when you are already in presentation mode.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could add a section about sharing the story map made easier with specta and JupyterLite. For that we'd need a minimal setup, maybe in a separate repo, of specta + jupyterlite + jupytergis. I can help with that if you need.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's also leave that for futur us and not block this PR with this

Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { IJGISLayer, IJupyterGISModel } from '@jupytergis/schema';
import { useMemo, useRef } from 'react';

import {
getEffectiveSymbologyParams,
type IEffectiveSymbologyParams,
} from '../symbologyUtils';

export interface IUseEffectiveSymbologyParamsArgs {
model: IJupyterGISModel;
layerId: string | undefined;
layer: IJGISLayer | null | undefined;
isStorySegmentOverride?: boolean;
segmentId?: string;
}

/**
* Resolve the effective symbology params (layer.parameters or segment override)
* for the current dialog context. Pass a type parameter to narrow the return
* type for the layer kind this component uses (e.g. VectorSymbologyParams for
* vector symbology components).
*/
export function useEffectiveSymbologyParams<
T extends IEffectiveSymbologyParams = IEffectiveSymbologyParams,
>({
model,
layerId,
layer,
isStorySegmentOverride,
segmentId,
}: IUseEffectiveSymbologyParamsArgs): T | null {
const result = useMemo(() => {
if (!layerId || !layer) {
return null;
}
return getEffectiveSymbologyParams(
model,
layerId,
layer,
isStorySegmentOverride,
segmentId,
);
}, [model, layerId, layer, isStorySegmentOverride, segmentId]);

// Stabilize reference
const prevRef = useRef<{
value: IEffectiveSymbologyParams | null;
serialized: string;
}>({ value: null, serialized: '' });
const serialized = result === null ? '' : JSON.stringify(result);
if (serialized === prevRef.current.serialized) {
return prevRef.current.value as T | null;
}
prevRef.current = { value: result, serialized };
return result as T | null;
}
39 changes: 39 additions & 0 deletions packages/base/src/dialogs/symbology/hooks/useOkSignal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Signal } from '@lumino/signaling';
import { useCallback, useEffect, useRef } from 'react';

type OkSignalPromise = {
promise: Promise<Signal<any, null>>;
};

export function useOkSignal(
okSignalPromise: OkSignalPromise,
handleOk: () => void,
): void {
const handleOkRef = useRef(handleOk);

useEffect(() => {
handleOkRef.current = handleOk;
}, [handleOk]);

const slot = useCallback(() => {
handleOkRef.current();
}, []);

useEffect(() => {
let disposed = false;

okSignalPromise.promise.then(okSignal => {
if (disposed) {
return;
}
okSignal.connect(slot);
});

return () => {
disposed = true;
okSignalPromise.promise.then(okSignal => {
okSignal.disconnect(slot);
});
};
}, [okSignalPromise, slot]);
}
35 changes: 16 additions & 19 deletions packages/base/src/dialogs/symbology/symbologyDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import VectorRendering from './vector_layer/VectorRendering';

export interface ISymbologyDialogProps {
model: IJupyterGISModel;
state: IStateDB;
okSignalPromise: PromiseDelegate<Signal<SymbologyWidget, null>>;
cancel: () => void;
layerId?: string;
isStorySegmentOverride?: boolean;
segmentId?: string;
}

export interface ISymbologyDialogWithAttributesProps extends ISymbologyDialogProps {
Expand All @@ -31,6 +31,8 @@ export type ISymbologyTabbedDialogWithAttributesProps =
export interface ISymbologyWidgetOptions {
model: IJupyterGISModel;
state: IStateDB;
isStorySegmentOverride?: boolean;
segmentId?: string;
}

export interface IStopRow {
Expand All @@ -40,9 +42,9 @@ export interface IStopRow {

const SymbologyDialog: React.FC<ISymbologyDialogProps> = ({
model,
state,
okSignalPromise,
cancel,
isStorySegmentOverride,
segmentId,
}) => {
const [selectedLayer, setSelectedLayer] = useState<string | null>(null);
const [componentToRender, setComponentToRender] =
Expand Down Expand Up @@ -90,21 +92,21 @@ const SymbologyDialog: React.FC<ISymbologyDialogProps> = ({
LayerSymbology = (
<VectorRendering
model={model}
state={state}
okSignalPromise={okSignalPromise}
cancel={cancel}
layerId={selectedLayer}
isStorySegmentOverride={isStorySegmentOverride}
segmentId={segmentId}
/>
);
break;
case 'WebGlLayer':
LayerSymbology = (
<TiffRendering
model={model}
state={state}
okSignalPromise={okSignalPromise}
cancel={cancel}
layerId={selectedLayer}
isStorySegmentOverride={isStorySegmentOverride}
segmentId={segmentId}
/>
);
break;
Expand All @@ -121,10 +123,6 @@ export class SymbologyWidget extends Dialog<boolean> {
private okSignal: Signal<SymbologyWidget, null>;

constructor(options: ISymbologyWidgetOptions) {
const cancelCallback = () => {
this.resolve(0);
};

const okSignalPromise = new PromiseDelegate<
Signal<SymbologyWidget, null>
>();
Expand All @@ -133,29 +131,28 @@ export class SymbologyWidget extends Dialog<boolean> {
<SymbologyDialog
model={options.model}
okSignalPromise={okSignalPromise}
cancel={cancelCallback}
state={options.state}
isStorySegmentOverride={options.isStorySegmentOverride}
segmentId={options.segmentId}
/>
);

super({ title: 'Symbology', body });

this.id = 'jupytergis::symbologyWidget';

this.okSignal = new Signal(this);

okSignalPromise.resolve(this.okSignal);

this.addClass('jp-gis-symbology-dialog');
}

resolve(index: number): void {
if (index === 0) {
super.resolve(index);
}

if (index === 1) {
// Emit signal to let symbology components save
this.okSignal.emit(null);
}

super.resolve(index);
}
}

Expand Down
Loading
Loading