Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
2d325bd
Add command
gjmooney Oct 24, 2025
9007bef
Add landmarks as a layer
gjmooney Oct 24, 2025
e164a2e
Move to landmark wip
gjmooney Oct 24, 2025
9f0e86d
Add a zoom to layer button
gjmooney Oct 28, 2025
d81e2ff
Add a story map thing to the file
gjmooney Oct 28, 2025
098a9f9
Add temp button to create story
gjmooney Oct 28, 2025
0fc0433
Add story creation method to model
gjmooney Oct 28, 2025
d6fc582
Add story editing(?) panel
gjmooney Oct 28, 2025
208dba2
Rename panel
gjmooney Oct 28, 2025
a25f488
Add storiesMap to file
gjmooney Oct 29, 2025
a8783f3
Add some panels
gjmooney Oct 29, 2025
cdb9ad8
Add temp viewer panel
gjmooney Oct 29, 2025
118e887
Start implementing story viewer
gjmooney Oct 29, 2025
e14bfba
Use textarea for markdown field
gjmooney Oct 29, 2025
a55b3bf
Push zoom button to right side
gjmooney Oct 29, 2025
62247d3
Only support one story
gjmooney Nov 3, 2025
3ff1d43
Handle story signal stuff
gjmooney Nov 3, 2025
ce08f0f
Update landmark order when drag and dropping
gjmooney Nov 4, 2025
d2751e5
Update viewer order based on list order
gjmooney Nov 4, 2025
e662807
Tidy
gjmooney Nov 4, 2025
8af6dd0
Hack story selection
gjmooney Nov 4, 2025
5f8d15b
Effect cleaning
gjmooney Nov 4, 2025
e60ba5c
Return ID as well
gjmooney Nov 4, 2025
daa93a7
Refactor viewer panel
gjmooney Nov 4, 2025
df196fb
Refactor editor panel
gjmooney Nov 4, 2025
b770d70
Dont reverse landmark list
gjmooney Nov 5, 2025
fbf15ab
Update viewer content when selecting landmarks
gjmooney Nov 5, 2025
cc22510
Remove rank from schema
gjmooney Nov 5, 2025
b1d86b3
Update viewer panel style
gjmooney Nov 5, 2025
56d4c09
Move to position when clicking landmark in unguided tour
gjmooney Nov 5, 2025
ffa4943
Remove zoomies
gjmooney Nov 5, 2025
8934439
Fix status bar loading when adding landmark
gjmooney Nov 6, 2025
f13f695
Use one panel for both story panels
gjmooney Nov 6, 2025
ed125ae
Refactor editor panel
gjmooney Nov 7, 2025
955755c
Remove stray console logs
gjmooney Nov 7, 2025
efd353c
Add story presentation as a setting
gjmooney Nov 7, 2025
c6216f3
Add presentation flags to side panels
gjmooney Nov 7, 2025
8e7baef
Some styling
gjmooney Nov 10, 2025
f52792d
see ess ess
gjmooney Nov 11, 2025
e59b878
Add button to recenter landmark
gjmooney Nov 14, 2025
113b4ae
Remove hardcoded id
gjmooney Nov 17, 2025
6b2f8cc
Clean up
gjmooney Nov 19, 2025
8357191
Format-on-save, why hast thou forsaken me?
gjmooney Nov 19, 2025
a08e435
Landmark icon
gjmooney Nov 20, 2025
0c55da5
Allow customization for transition animations
gjmooney Nov 20, 2025
412a2c7
Change preview button to a switch
gjmooney Nov 20, 2025
a401df6
Move nav buttons
gjmooney Nov 20, 2025
0b5eabf
Cancel animations before starting another
gjmooney Nov 20, 2025
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
2 changes: 2 additions & 0 deletions packages/base/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-popover": "^1.1.14",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-toggle-group": "^1.1.10",
"@rjsf/core": "^4.2.0",
Expand All @@ -93,6 +94,7 @@
"proj4-list": "1.0.4",
"react": "^18.0.1",
"react-day-picker": "^9.7.0",
"react-markdown": "^10.1.0",
"shpjs": "^6.1.0",
"styled-components": "^5.3.6",
"three": "^0.135.0",
Expand Down
3 changes: 3 additions & 0 deletions packages/base/src/commands/BaseCommandIDs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,6 @@ export const showFiltersTab = 'jupytergis:showFiltersTab';
export const showObjectPropertiesTab = 'jupytergis:showObjectPropertiesTab';
export const showAnnotationsTab = 'jupytergis:showAnnotationsTab';
export const showIdentifyPanelTab = 'jupytergis:showIdentifyPanelTab';

// Story maps
export const addLandmark = 'jupytergis:addLandmark';
67 changes: 66 additions & 1 deletion packages/base/src/commands/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import {
IDict,
IJGISFormSchemaRegistry,
IJGISLayer,
IJGISLayerBrowserRegistry,
IJGISLayerGroup,
IJGISLayerItem,
IJGISStoryMap,
IJupyterGISModel,
ILandmarkLayer,
JgisCoordinates,
LayerType,
SelectionType,
Expand All @@ -16,7 +19,7 @@ import { ICompletionProviderManager } from '@jupyterlab/completer';
import { IStateDB } from '@jupyterlab/statedb';
import { ITranslator } from '@jupyterlab/translation';
import { CommandRegistry } from '@lumino/commands';
import { ReadonlyPartialJSONObject } from '@lumino/coreutils';
import { ReadonlyPartialJSONObject, UUID } from '@lumino/coreutils';
import { Coordinate } from 'ol/coordinate';
import { fromLonLat } from 'ol/proj';

Expand Down Expand Up @@ -1055,6 +1058,68 @@ export function addCommands(
...icons.get(CommandIDs.addMarker),
});

commands.addCommand(CommandIDs.addLandmark, {
label: trans.__('Add Landmark'),
isEnabled: () => {
return tracker.currentWidget
? tracker.currentWidget.model.sharedModel.editable
: false;
},
execute: args => {
const storyMapId = UUID.uuid4();
const newLandmarkId = UUID.uuid4();
const current = tracker.currentWidget;
if (!current) {
return;
}
const { zoom, extent } = current.model.getOptions();

if (!zoom || !extent) {
console.log('No extent or zoom found');
return;
}

const layerParams: ILandmarkLayer = { extent, zoom };
const layerModel: IJGISLayer = {
type: 'LandmarkLayer',
visible: true,
name: 'Landmark',
parameters: layerParams,
};

current.model.addLayer(newLandmarkId, layerModel);

// check for stories
const isStoriesExist =
Object.keys(current.model.sharedModel.storiesMap).length !== 0;

// if not stories, then just add simple
if (!isStoriesExist) {
const title = 'New Story';
const storyType = 'guided';
const landmarks = [newLandmarkId];

const storyMap: IJGISStoryMap = { title, storyType, landmarks };

current.model.sharedModel.addStoryMap(storyMapId, storyMap);
} else {
// else need to update stories
const { story } = current.model.getSelectedStory();
if (!story) {
console.log('No story found, something went wrong');
return;
}
const newStory: IJGISStoryMap = {
...story,
landmarks: [...(story.landmarks ?? []), newLandmarkId],
};

current.model.sharedModel.updateStoryMap(storyMapId, newStory);
}
},
...icons.get(CommandIDs.addLandmark),
});

loadKeybindings(commands, keybindings);
}

Expand Down
1 change: 1 addition & 0 deletions packages/base/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ const iconObject = {
[CommandIDs.identify]: { icon: infoIcon },
[CommandIDs.temporalController]: { icon: clockIcon },
[CommandIDs.addMarker]: { icon: markerIcon },
[CommandIDs.addLandmark]: { iconClass: 'fa fa-landmark' },
};

/**
Expand Down
6 changes: 6 additions & 0 deletions packages/base/src/formbuilder/formselectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { LayerType, SourceType } from '@jupytergis/schema';
import {
HeatmapLayerPropertiesForm,
HillshadeLayerPropertiesForm,
LandmarkLayerPropertiesForm,
LayerPropertiesForm,
VectorLayerPropertiesForm,
WebGlLayerPropertiesForm,
Expand Down Expand Up @@ -33,6 +34,11 @@ export function getLayerTypeForm(
break;
case 'HeatmapLayer':
LayerForm = HeatmapLayerPropertiesForm;
break;
case 'LandmarkLayer':
LayerForm = LandmarkLayerPropertiesForm;
break;

// ADD MORE FORM TYPES HERE
}

Expand Down
1 change: 1 addition & 0 deletions packages/base/src/formbuilder/objectform/layer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './hillshadeLayerForm';
export * from './layerform';
export * from './vectorlayerform';
export * from './webGlLayerForm';
export * from './landmarkLayerForm';
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { IDict } from '@jupytergis/schema';

import { LayerPropertiesForm } from './layerform';

/**
* The form to modify a hillshade layer.
*/
export class LandmarkLayerPropertiesForm extends LayerPropertiesForm {
protected processSchema(
data: IDict<any> | undefined,
schema: IDict,
uiSchema: IDict,
) {
super.processSchema(data, schema, uiSchema);
uiSchema['content'] = {
...uiSchema['content'],
markdown: {
'ui:widget': 'textarea',
'ui:options': {
rows: 10,
},
},
};
}
}
96 changes: 74 additions & 22 deletions packages/base/src/mainview/mainView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
JgisCoordinates,
JupyterGISModel,
IMarkerSource,
ILandmarkLayer,
} from '@jupytergis/schema';
import { showErrorMessage } from '@jupyterlab/apputils';
import { IObservableMap, ObservableMap } from '@jupyterlab/observables';
Expand All @@ -53,6 +54,7 @@ import Feature, { FeatureLike } from 'ol/Feature';
import { FullScreen, ScaleLine } from 'ol/control';
import { Coordinate } from 'ol/coordinate';
import { singleClick } from 'ol/events/condition';
import { getCenter } from 'ol/extent';
import { GeoJSON, MVT } from 'ol/format';
import { Geometry, Point } from 'ol/geom';
import { DragAndDrop, Select } from 'ol/interaction';
Expand Down Expand Up @@ -995,7 +997,8 @@ export class MainView extends React.Component<IProps, IStates> {
let sourceId: string | undefined;
let source: IJGISSource | undefined;

if (layer.type !== 'StacLayer') {
// Sourceless layers
if (!['StacLayer', 'LandmarkLayer'].includes(layer.type)) {
sourceId = layer.parameters?.source;
if (!sourceId) {
return;
Expand Down Expand Up @@ -1119,6 +1122,11 @@ export class MainView extends React.Component<IProps, IStates> {

break;
}

case 'LandmarkLayer': {
// Special layer not for this
return;
}
}

// OpenLayers doesn't have name/id field so add it
Expand Down Expand Up @@ -1200,7 +1208,6 @@ export class MainView extends React.Component<IProps, IStates> {
item => item.id === id && item.error === error.message,
)
) {
this._loadingLayers.delete(id);
return;
}

Expand All @@ -1214,7 +1221,9 @@ export class MainView extends React.Component<IProps, IStates> {
error: error.message || 'invalid file path',
index,
});
} finally {
this._loadingLayers.delete(id);
this.setState(old => ({ ...old, loadingLayer: false }));
}
}

Expand Down Expand Up @@ -1656,7 +1665,7 @@ export class MainView extends React.Component<IProps, IStates> {
const { x, y } = remoteViewport.value.coordinates;
const zoom = remoteViewport.value.zoom;

this._moveToPosition({ x, y }, zoom, 0);
this._flyToPosition({ x, y }, zoom, 0);
}
} else {
// If we are unfollowing a remote user, we reset our center and zoom to their previous values
Expand All @@ -1668,7 +1677,7 @@ export class MainView extends React.Component<IProps, IStates> {
const viewportState = localState.viewportState?.value;

if (viewportState) {
this._moveToPosition(viewportState.coordinates, viewportState.zoom);
this._flyToPosition(viewportState.coordinates, viewportState.zoom);
}
}
}
Expand Down Expand Up @@ -1790,7 +1799,7 @@ export class MainView extends React.Component<IProps, IStates> {
view.getProjection(),
);

this._moveToPosition({ x: centerCoord[0], y: centerCoord[1] }, zoom || 0);
this._flyToPosition({ x: centerCoord[0], y: centerCoord[1] }, zoom || 0);

// Save the extent if it does not exists, to allow proper export to qgis.
if (!options.extent) {
Expand Down Expand Up @@ -2022,6 +2031,7 @@ export class MainView extends React.Component<IProps, IStates> {
});
}

// TODO this and flyToPosition need a rework
private _onZoomToPosition(_: IJupyterGISModel, id: string) {
// Check if the id is an annotation
const annotation = this._model.annotationModel?.getAnnotation(id);
Expand All @@ -2035,6 +2045,26 @@ export class MainView extends React.Component<IProps, IStates> {
const layer = this.getLayer(id);
const source = layer?.getSource();

// TODO: Landmark layers don't have an associated OL layer
// This could be better
if (!layer) {
const jgisLayer = this._model.getLayer(id);
const layerParams = jgisLayer?.parameters as ILandmarkLayer;
const coords = getCenter(layerParams.extent);

// TODO: Should pass args through signal??
const { story } = this._model.getSelectedStory();

this._flyToPosition(
{ x: coords[0], y: coords[1] },
layerParams.zoom,
(story?.transition?.time ?? 1) * 1000, // second -> ms
story?.transition?.type,
);

return;
}

if (source instanceof VectorSource) {
extent = source.getExtent();
}
Expand Down Expand Up @@ -2069,35 +2099,57 @@ export class MainView extends React.Component<IProps, IStates> {
});
}

private _moveToPosition(
private _flyToPosition(
center: { x: number; y: number },
zoom: number,
duration = 1000,
transitionType?: 'linear' | 'immediate' | 'smooth',
) {
const view = this._Map.getView();

view.setZoom(zoom);
view.setCenter([center.x, center.y]);
// Zoom needs to be set before changing center
if (!view.animate === undefined) {
view.animate({ zoom, duration });
// Cancel any in-progress animations before starting new ones
view.cancelAnimations();

const currentZoom = view.getZoom() || 0;
const targetCenter: Coordinate = [center.x, center.y];

if (transitionType === 'immediate') {
view.setCenter(targetCenter);
view.setZoom(zoom);
return;
}

if (transitionType === 'smooth') {
// Smooth: zoom out, center, and zoom in
// Centering takes full duration, zoom out completes halfway, zoom in starts halfway
const zoomOutLevel = Math.min(currentZoom, zoom) - 1;

// Start centering (full duration) and zoom out (50% duration) simultaneously
view.animate({
center: targetCenter,
duration: duration,
});
// Chain zoom out -> zoom in (zoom in starts when zoom out completes)
view.animate(
{
zoom: zoomOutLevel,
duration: duration * 0.5,
},
{
zoom: zoom,
duration: duration * 0.5,
},
);
} else {
// Linear: direct zoom
view.animate({
center: [center.x, center.y],
center: targetCenter,
zoom: zoom,
duration,
});
}
}

private _flyToPosition(
center: { x: number; y: number },
zoom: number,
duration = 1000,
) {
const view = this._Map.getView();
view.animate({ zoom, duration });
view.animate({ center: [center.x, center.y], duration });
}

private _onPointerMove(e: MouseEvent) {
const pixel = this._Map.getEventPixel(e);
const coordinates = this._Map.getCoordinateFromPixel(pixel);
Expand Down
Loading
Loading