@jupytergis/base 0.4.4 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,4 @@
1
1
  import React from 'react';
2
2
  import { IAnnotationProps } from './Annotation';
3
- interface IAnnotationFloaterProps extends IAnnotationProps {
4
- open: boolean;
5
- }
6
- declare const AnnotationFloater: ({ itemId, annotationModel: model, open }: IAnnotationFloaterProps) => React.JSX.Element;
3
+ declare const AnnotationFloater: ({ itemId, annotationModel: model }: IAnnotationProps) => React.JSX.Element;
7
4
  export default AnnotationFloater;
@@ -2,21 +2,26 @@ import React, { useState } from 'react';
2
2
  import { faWindowMinimize } from '@fortawesome/free-solid-svg-icons';
3
3
  import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
4
4
  import Annotation from './Annotation';
5
- const AnnotationFloater = ({ itemId, annotationModel: model, open }) => {
6
- const [isOpen, setIsOpen] = useState(open);
5
+ const AnnotationFloater = ({ itemId, annotationModel: model }) => {
6
+ const annotation = model.getAnnotation(itemId);
7
+ const [isOpen, setIsOpen] = useState(annotation === null || annotation === void 0 ? void 0 : annotation.open);
7
8
  // Function that either
8
9
  // - opens the annotation if `open`
9
10
  // - removes the annotation if `!open` and the annotation is empty
10
11
  // - closes the annotation if `!open` and the annotation is not empty
11
12
  const setOpenOrDelete = (open) => {
12
- var _a;
13
13
  if (open) {
14
+ model.updateAnnotation(itemId, { open: true });
14
15
  return setIsOpen(true);
15
16
  }
16
- if (!((_a = model.getAnnotation(itemId)) === null || _a === void 0 ? void 0 : _a.contents.length)) {
17
- return model.removeAnnotation(itemId);
17
+ const current = model.getAnnotation(itemId);
18
+ if (!(current === null || current === void 0 ? void 0 : current.contents.length)) {
19
+ model.removeAnnotation(itemId);
20
+ }
21
+ else {
22
+ model.updateAnnotation(itemId, { open: false });
23
+ setIsOpen(false);
18
24
  }
19
- setIsOpen(false);
20
25
  };
21
26
  return (React.createElement(React.Fragment, null,
22
27
  React.createElement("div", { className: "jGIS-Annotation-Handler", onClick: () => setOpenOrDelete(!isOpen) }),
@@ -12,6 +12,7 @@ export declare class AnnotationModel implements IAnnotationModel {
12
12
  getAnnotation(id: string): IAnnotation | undefined;
13
13
  getAnnotationIds(): string[];
14
14
  addAnnotation(key: string, value: IAnnotation): void;
15
+ updateAnnotation(id: string, updates: Partial<IAnnotation>): void;
15
16
  removeAnnotation(key: string): void;
16
17
  addContent(id: string, value: string): void;
17
18
  private _model;
@@ -48,6 +48,15 @@ export class AnnotationModel {
48
48
  var _a;
49
49
  (_a = this._model) === null || _a === void 0 ? void 0 : _a.sharedModel.setMetadata(`annotation_${key}`, JSON.stringify(value));
50
50
  }
51
+ updateAnnotation(id, updates) {
52
+ var _a;
53
+ const existing = this.getAnnotation(id);
54
+ if (!existing) {
55
+ return;
56
+ }
57
+ (_a = this._model) === null || _a === void 0 ? void 0 : _a.sharedModel.setMetadata(id, JSON.stringify(Object.assign(Object.assign({}, existing), updates)));
58
+ this._updateSignal.emit(null);
59
+ }
51
60
  removeAnnotation(key) {
52
61
  var _a;
53
62
  (_a = this._model) === null || _a === void 0 ? void 0 : _a.removeMetadata(key);
package/lib/commands.js CHANGED
@@ -8,6 +8,8 @@ import { JupyterGISDocumentWidget } from './widget';
8
8
  import { getGeoJSONDataFromLayerSource, downloadFile } from './tools';
9
9
  import { ProcessingFormDialog } from './dialogs/ProcessingFormDialog';
10
10
  import { getSingleSelectedLayer, selectedLayerIsOfType, processSelectedLayer } from './processing';
11
+ import { fromLonLat } from 'ol/proj';
12
+ import { targetWithCenterIcon } from './icons';
11
13
  function loadKeybindings(commands, keybindings) {
12
14
  keybindings.forEach(binding => {
13
15
  commands.addKeyBinding({
@@ -842,6 +844,36 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
842
844
  downloadFile(geojsonString, `${exportFileName}.geojson`, 'application/geo+json');
843
845
  }
844
846
  });
847
+ commands.addCommand(CommandIDs.getGeolocation, {
848
+ label: trans.__('Center on Geolocation'),
849
+ execute: async () => {
850
+ var _a;
851
+ const viewModel = (_a = tracker.currentWidget) === null || _a === void 0 ? void 0 : _a.model;
852
+ const options = {
853
+ enableHighAccuracy: true,
854
+ timeout: 5000,
855
+ maximumAge: 0
856
+ };
857
+ const success = (pos) => {
858
+ const location = fromLonLat([
859
+ pos.coords.longitude,
860
+ pos.coords.latitude
861
+ ]);
862
+ const Jgislocation = {
863
+ x: location[0],
864
+ y: location[1]
865
+ };
866
+ if (viewModel) {
867
+ viewModel.geolocationChanged.emit(Jgislocation);
868
+ }
869
+ };
870
+ const error = (err) => {
871
+ console.warn(`ERROR(${err.code}): ${err.message}`);
872
+ };
873
+ navigator.geolocation.getCurrentPosition(success, error, options);
874
+ },
875
+ icon: targetWithCenterIcon
876
+ });
845
877
  loadKeybindings(commands, keybindings);
846
878
  }
847
879
  var Private;
@@ -9,6 +9,7 @@ export declare namespace CommandIDs {
9
9
  const symbology = "jupytergis:symbology";
10
10
  const identify = "jupytergis:identify";
11
11
  const temporalController = "jupytergis:temporalController";
12
+ const getGeolocation = "jupytergis:getGeolocation";
12
13
  const openLayerBrowser = "jupytergis:openLayerBrowser";
13
14
  const newRasterEntry = "jupytergis:newRasterEntry";
14
15
  const newVectorTileEntry = "jupytergis:newVectorTileEntry";
package/lib/constants.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { redoIcon, undoIcon } from '@jupyterlab/ui-components';
2
- import { geoJSONIcon, moundIcon, rasterIcon } from './icons';
2
+ import { bookOpenIcon, clockIcon, geoJSONIcon, infoIcon, moundIcon, rasterIcon, vectorSquareIcon } from './icons';
3
3
  /**
4
4
  * The command IDs.
5
5
  */
@@ -11,6 +11,8 @@ export var CommandIDs;
11
11
  CommandIDs.symbology = 'jupytergis:symbology';
12
12
  CommandIDs.identify = 'jupytergis:identify';
13
13
  CommandIDs.temporalController = 'jupytergis:temporalController';
14
+ // geolocation
15
+ CommandIDs.getGeolocation = 'jupytergis:getGeolocation';
14
16
  // Layers and sources creation commands
15
17
  CommandIDs.openLayerBrowser = 'jupytergis:openLayerBrowser';
16
18
  // Layer and source
@@ -78,9 +80,9 @@ const iconObject = {
78
80
  VideoLayer: { iconClass: 'fa fa-video' },
79
81
  [CommandIDs.redo]: { icon: redoIcon },
80
82
  [CommandIDs.undo]: { icon: undoIcon },
81
- [CommandIDs.openLayerBrowser]: { iconClass: 'fa fa-book-open' },
83
+ [CommandIDs.openLayerBrowser]: { icon: bookOpenIcon },
82
84
  [CommandIDs.newRasterEntry]: { icon: rasterIcon },
83
- [CommandIDs.newVectorTileEntry]: { iconClass: 'fa fa-vector-square' },
85
+ [CommandIDs.newVectorTileEntry]: { icon: vectorSquareIcon },
84
86
  [CommandIDs.newGeoJSONEntry]: { icon: geoJSONIcon },
85
87
  [CommandIDs.newHillshadeEntry]: { icon: moundIcon },
86
88
  [CommandIDs.newImageEntry]: { iconClass: 'fa fa-image' },
@@ -88,8 +90,8 @@ const iconObject = {
88
90
  [CommandIDs.newShapefileLayer]: { iconClass: 'fa fa-file' },
89
91
  [CommandIDs.newGeoTiffEntry]: { iconClass: 'fa fa-image' },
90
92
  [CommandIDs.symbology]: { iconClass: 'fa fa-brush' },
91
- [CommandIDs.identify]: { iconClass: 'fa fa-info' },
92
- [CommandIDs.temporalController]: { iconClass: 'fa fa-clock' }
93
+ [CommandIDs.identify]: { icon: infoIcon },
94
+ [CommandIDs.temporalController]: { icon: clockIcon }
93
95
  };
94
96
  /**
95
97
  * The registered icons
package/lib/icons.d.ts CHANGED
@@ -8,3 +8,11 @@ export declare const nonVisibilityIcon: LabIcon;
8
8
  export declare const geoJSONIcon: LabIcon;
9
9
  export declare const moundIcon: LabIcon;
10
10
  export declare const logoMiniIconQGZ: LabIcon;
11
+ export declare const bookOpenIcon: LabIcon;
12
+ export declare const vectorSquareIcon: LabIcon;
13
+ export declare const infoIcon: LabIcon;
14
+ export declare const clockIcon: LabIcon;
15
+ export declare const terminalToolbarIcon: LabIcon;
16
+ export declare const geolocationIcon: LabIcon;
17
+ export declare const targetWithoutCenterIcon: LabIcon;
18
+ export declare const targetWithCenterIcon: LabIcon;
package/lib/icons.js CHANGED
@@ -13,6 +13,14 @@ import nonVisibilitySvgStr from '../style/icons/nonvisibility.svg';
13
13
  import geoJsonSvgStr from '../style/icons/geojson.svg';
14
14
  import moundSvgStr from '../style/icons/mound.svg';
15
15
  import logoMiniQGZ from '../style/icons/logo_mini_qgz.svg';
16
+ import bookOpenSvgStr from '../style/icons/book_open.svg';
17
+ import vectorSquareSvgStr from '../style/icons/vector_square.svg';
18
+ import infoSvgStr from '../style/icons/info-solid.svg';
19
+ import clockSvgStr from '../style/icons/clock-solid.svg';
20
+ import terminalToolbarSvgStr from '../style/icons/terminal_toolbar.svg';
21
+ import geolocationSvgStr from '../style/icons/geolocation.svg';
22
+ import targetWithoutCenterSvgStr from '../style/icons/target_without_center.svg';
23
+ import targetWithCenterSvgStr from '../style/icons/target_with_center.svg';
16
24
  export const logoIcon = new LabIcon({
17
25
  name: 'jupytergis::logo',
18
26
  svgstr: logoSvgStr
@@ -49,3 +57,35 @@ export const logoMiniIconQGZ = new LabIcon({
49
57
  name: 'jupytergis::logoQGZ',
50
58
  svgstr: logoMiniQGZ
51
59
  });
60
+ export const bookOpenIcon = new LabIcon({
61
+ name: 'jupytergis::bookOpen',
62
+ svgstr: bookOpenSvgStr
63
+ });
64
+ export const vectorSquareIcon = new LabIcon({
65
+ name: 'jupytergis::vectorSquare',
66
+ svgstr: vectorSquareSvgStr
67
+ });
68
+ export const infoIcon = new LabIcon({
69
+ name: 'jupytergis::info',
70
+ svgstr: infoSvgStr
71
+ });
72
+ export const clockIcon = new LabIcon({
73
+ name: 'jupytergis::clock',
74
+ svgstr: clockSvgStr
75
+ });
76
+ export const terminalToolbarIcon = new LabIcon({
77
+ name: 'jupytergis::terminalToolbar',
78
+ svgstr: terminalToolbarSvgStr
79
+ });
80
+ export const geolocationIcon = new LabIcon({
81
+ name: 'jupytergis::geolocation',
82
+ svgstr: geolocationSvgStr
83
+ });
84
+ export const targetWithoutCenterIcon = new LabIcon({
85
+ name: 'jupytergis::targetWithCenter',
86
+ svgstr: targetWithCenterSvgStr
87
+ });
88
+ export const targetWithCenterIcon = new LabIcon({
89
+ name: 'jupytergis::targetWithoutCenter',
90
+ svgstr: targetWithoutCenterSvgStr
91
+ });
@@ -33,7 +33,7 @@ export declare class MainView extends React.Component<IProps, IStates> {
33
33
  constructor(props: IProps);
34
34
  componentDidMount(): Promise<void>;
35
35
  componentWillUnmount(): void;
36
- generateScene(): Promise<void>;
36
+ generateScene(center: number[], zoom: number): Promise<void>;
37
37
  createSelectInteraction: () => void;
38
38
  addContextMenu: () => void;
39
39
  /**
@@ -110,6 +110,8 @@ export declare class MainView extends React.Component<IProps, IStates> {
110
110
  * to work with the temporal controller
111
111
  */
112
112
  handleTemporalController: (id: string, layer: IJGISLayer) => void;
113
+ private flyToGeometry;
114
+ private highlightFeatureOnMap;
113
115
  /**
114
116
  * Wait for all layers to be loaded.
115
117
  */
@@ -165,11 +167,13 @@ export declare class MainView extends React.Component<IProps, IStates> {
165
167
  private _updateAnnotation;
166
168
  private _onZoomToPosition;
167
169
  private _moveToPosition;
170
+ private _flyToPosition;
168
171
  private _onPointerMove;
169
172
  private _syncPointer;
170
173
  private _identifyFeature;
171
174
  private _triggerLayerUpdate;
172
175
  private _convertFeatureToMs;
176
+ private _handleGeolocationChanged;
173
177
  private _handleThemeChange;
174
178
  private _handleWindowResize;
175
179
  render(): JSX.Element;
@@ -187,5 +191,6 @@ export declare class MainView extends React.Component<IProps, IStates> {
187
191
  private _contextMenu;
188
192
  private _loadingLayers;
189
193
  private _originalFeatures;
194
+ private _highlightLayer;
190
195
  }
191
196
  export {};
@@ -6,6 +6,7 @@ import { ContextMenu } from '@lumino/widgets';
6
6
  import { Collection, Map as OlMap, View, getUid } from 'ol';
7
7
  //@ts-expect-error no types for ol-pmtiles
8
8
  import { PMTilesRasterSource, PMTilesVectorSource } from 'ol-pmtiles';
9
+ import Feature from 'ol/Feature';
9
10
  import { ScaleLine } from 'ol/control';
10
11
  import { singleClick } from 'ol/events/condition';
11
12
  import { GeoJSON, MVT } from 'ol/format';
@@ -31,6 +32,7 @@ import CollaboratorPointers from './CollaboratorPointers';
31
32
  import { FollowIndicator } from './FollowIndicator';
32
33
  import TemporalSlider from './TemporalSlider';
33
34
  import { Spinner } from './spinner';
35
+ import { Point } from 'ol/geom';
34
36
  export class MainView extends React.Component {
35
37
  constructor(props) {
36
38
  super(props);
@@ -114,7 +116,8 @@ export class MainView extends React.Component {
114
116
  zoom: (_a = this._Map.getView().getZoom()) !== null && _a !== void 0 ? _a : 0,
115
117
  label: 'New annotation',
116
118
  contents: [],
117
- parent: this._Map.getViewport().id
119
+ parent: this._Map.getViewport().id,
120
+ open: true
118
121
  });
119
122
  },
120
123
  label: 'Add annotation',
@@ -418,6 +421,17 @@ export class MainView extends React.Component {
418
421
  this._model.zoomToPositionSignal.connect(this._onZoomToPosition, this);
419
422
  this._model.updateLayerSignal.connect(this._triggerLayerUpdate, this);
420
423
  this._model.addFeatureAsMsSignal.connect(this._convertFeatureToMs, this);
424
+ this._model.geolocationChanged.connect(this._handleGeolocationChanged, this);
425
+ this._model.flyToGeometrySignal.connect(this.flyToGeometry, this);
426
+ this._model.highlightFeatureSignal.connect(this.highlightFeatureOnMap, this);
427
+ // Watch isIdentifying and clear the highlight when Identify Tool is turned off
428
+ this._model.sharedModel.awareness.on('change', () => {
429
+ var _a;
430
+ const isIdentifying = this._model.isIdentifying;
431
+ if (!isIdentifying && this._highlightLayer) {
432
+ (_a = this._highlightLayer.getSource()) === null || _a === void 0 ? void 0 : _a.clear();
433
+ }
434
+ });
421
435
  this.state = {
422
436
  id: this._mainViewModel.id,
423
437
  lightTheme: isLightTheme(),
@@ -439,7 +453,12 @@ export class MainView extends React.Component {
439
453
  }
440
454
  async componentDidMount() {
441
455
  window.addEventListener('resize', this._handleWindowResize);
442
- await this.generateScene();
456
+ const options = this._model.getOptions();
457
+ const center = options.longitude !== undefined && options.latitude !== undefined
458
+ ? fromLonLat([options.longitude, options.latitude])
459
+ : [0, 0];
460
+ const zoom = options.zoom !== undefined ? options.zoom : 1;
461
+ await this.generateScene(center, zoom);
443
462
  this.addContextMenu();
444
463
  this._mainViewModel.initSignal();
445
464
  if (window.jupytergisMaps !== undefined && this._documentPath) {
@@ -457,14 +476,14 @@ export class MainView extends React.Component {
457
476
  this._model.clientStateChanged.disconnect(this._onClientSharedStateChanged, this);
458
477
  this._mainViewModel.dispose();
459
478
  }
460
- async generateScene() {
479
+ async generateScene(center, zoom) {
461
480
  if (this.divRef.current) {
462
481
  this._Map = new OlMap({
463
482
  target: this.divRef.current,
464
483
  layers: [],
465
484
  view: new View({
466
- center: [0, 0],
467
- zoom: 1
485
+ center,
486
+ zoom
468
487
  }),
469
488
  controls: [new ScaleLine()]
470
489
  });
@@ -1064,6 +1083,86 @@ export class MainView extends React.Component {
1064
1083
  }
1065
1084
  }
1066
1085
  }
1086
+ flyToGeometry(sender, geometry) {
1087
+ if (!geometry || typeof geometry.getExtent !== 'function') {
1088
+ console.warn('Invalid geometry for flyToGeometry:', geometry);
1089
+ return;
1090
+ }
1091
+ const view = this._Map.getView();
1092
+ const extent = geometry.getExtent();
1093
+ view.fit(extent, {
1094
+ padding: [50, 50, 50, 50],
1095
+ duration: 1000,
1096
+ maxZoom: 16
1097
+ });
1098
+ }
1099
+ highlightFeatureOnMap(sender, featureOrGeometry) {
1100
+ const geometry = (featureOrGeometry === null || featureOrGeometry === void 0 ? void 0 : featureOrGeometry.geometry) ||
1101
+ (featureOrGeometry === null || featureOrGeometry === void 0 ? void 0 : featureOrGeometry._geometry) ||
1102
+ featureOrGeometry;
1103
+ if (!geometry) {
1104
+ console.warn('No geometry found in feature:', featureOrGeometry);
1105
+ return;
1106
+ }
1107
+ const isOlGeometry = typeof geometry.getCoordinates === 'function';
1108
+ const parsedGeometry = isOlGeometry
1109
+ ? geometry
1110
+ : new GeoJSON().readGeometry(geometry, {
1111
+ featureProjection: this._Map.getView().getProjection()
1112
+ });
1113
+ const olFeature = new Feature(Object.assign({ geometry: parsedGeometry }, (geometry !== featureOrGeometry ? featureOrGeometry : {})));
1114
+ if (!this._highlightLayer) {
1115
+ this._highlightLayer = new VectorLayer({
1116
+ source: new VectorSource(),
1117
+ style: feature => {
1118
+ var _a;
1119
+ const geomType = (_a = feature.getGeometry()) === null || _a === void 0 ? void 0 : _a.getType();
1120
+ switch (geomType) {
1121
+ case 'Point':
1122
+ case 'MultiPoint':
1123
+ return new Style({
1124
+ image: new Circle({
1125
+ radius: 6,
1126
+ fill: new Fill({ color: 'rgba(255, 255, 0, 0.8)' }),
1127
+ stroke: new Stroke({ color: '#ff0', width: 2 })
1128
+ })
1129
+ });
1130
+ case 'LineString':
1131
+ case 'MultiLineString':
1132
+ return new Style({
1133
+ stroke: new Stroke({
1134
+ color: 'rgba(255, 255, 0, 0.8)',
1135
+ width: 3
1136
+ })
1137
+ });
1138
+ case 'Polygon':
1139
+ case 'MultiPolygon':
1140
+ return new Style({
1141
+ stroke: new Stroke({
1142
+ color: '#f00',
1143
+ width: 2
1144
+ }),
1145
+ fill: new Fill({
1146
+ color: 'rgba(255, 255, 0, 0.8)'
1147
+ })
1148
+ });
1149
+ default:
1150
+ return new Style({
1151
+ stroke: new Stroke({
1152
+ color: '#000',
1153
+ width: 2
1154
+ })
1155
+ });
1156
+ }
1157
+ },
1158
+ zIndex: 999
1159
+ });
1160
+ this._Map.addLayer(this._highlightLayer);
1161
+ }
1162
+ const source = this._highlightLayer.getSource();
1163
+ source === null || source === void 0 ? void 0 : source.clear();
1164
+ source === null || source === void 0 ? void 0 : source.addFeature(olFeature);
1165
+ }
1067
1166
  /**
1068
1167
  * Wait for all layers to be loaded.
1069
1168
  */
@@ -1302,7 +1401,7 @@ export class MainView extends React.Component {
1302
1401
  // Check if the id is an annotation
1303
1402
  const annotation = (_a = this._model.annotationModel) === null || _a === void 0 ? void 0 : _a.getAnnotation(id);
1304
1403
  if (annotation) {
1305
- this._moveToPosition(annotation.position, annotation.zoom);
1404
+ this._flyToPosition(annotation.position, annotation.zoom);
1306
1405
  return;
1307
1406
  }
1308
1407
  // The id is a layer
@@ -1334,15 +1433,18 @@ export class MainView extends React.Component {
1334
1433
  }
1335
1434
  _moveToPosition(center, zoom, duration = 1000) {
1336
1435
  const view = this._Map.getView();
1436
+ view.setZoom(zoom);
1437
+ view.setCenter([center.x, center.y]);
1337
1438
  // Zoom needs to be set before changing center
1338
1439
  if (!view.animate === undefined) {
1339
1440
  view.animate({ zoom, duration });
1340
1441
  view.animate({ center: [center.x, center.y], duration });
1341
1442
  }
1342
- else {
1343
- view.setZoom(zoom);
1344
- view.setCenter([center.x, center.y]);
1345
- }
1443
+ }
1444
+ _flyToPosition(center, zoom, duration = 1000) {
1445
+ const view = this._Map.getView();
1446
+ view.animate({ zoom, duration });
1447
+ view.animate({ center: [center.x, center.y], duration });
1346
1448
  }
1347
1449
  _onPointerMove(e) {
1348
1450
  const pixel = this._Map.getEventPixel(e);
@@ -1378,6 +1480,10 @@ export class MainView extends React.Component {
1378
1480
  // last element is alpha
1379
1481
  bandValues['Alpha'] = data[data.length - 1];
1380
1482
  this._model.syncIdentifiedFeatures([bandValues], this._mainViewModel.id);
1483
+ const coordinate = this._Map.getCoordinateFromPixel(e.pixel);
1484
+ const point = new Point(coordinate);
1485
+ // trigger highlight via signal
1486
+ this._model.highlightFeatureSignal.emit(point);
1381
1487
  break;
1382
1488
  }
1383
1489
  }
@@ -1404,6 +1510,16 @@ export class MainView extends React.Component {
1404
1510
  feature.set(`${selectedFeature}ms`, parsedTime);
1405
1511
  });
1406
1512
  }
1513
+ _handleGeolocationChanged(sender, newPosition) {
1514
+ const view = this._Map.getView();
1515
+ const zoom = view.getZoom();
1516
+ if (zoom) {
1517
+ this._flyToPosition(newPosition, zoom);
1518
+ }
1519
+ else {
1520
+ throw new Error('Could not move to geolocation, because current zoom is not defined.');
1521
+ }
1522
+ }
1407
1523
  render() {
1408
1524
  return (React.createElement(React.Fragment, null,
1409
1525
  Object.entries(this.state.annotations).map(([key, annotation]) => {
@@ -1415,7 +1531,7 @@ export class MainView extends React.Component {
1415
1531
  left: screenPosition.x,
1416
1532
  top: screenPosition.y
1417
1533
  }, className: 'jGIS-Popup-Wrapper' },
1418
- React.createElement(AnnotationFloater, { itemId: key, annotationModel: this._model.annotationModel, open: false }))));
1534
+ React.createElement(AnnotationFloater, { itemId: key, annotationModel: this._model.annotationModel }))));
1419
1535
  }),
1420
1536
  React.createElement("div", { className: "jGIS-Mainview-Container" },
1421
1537
  this.state.displayTemporalController && (React.createElement(TemporalSlider, { model: this._model, filterStates: this.state.filterStates })),
@@ -29,14 +29,14 @@ export class AnnotationsPanel extends Component {
29
29
  React.createElement(Annotation, { rightPanelModel: this._rightPanelModel, annotationModel: this._annotationModel, itemId: id }),
30
30
  React.createElement("hr", { className: "jGIS-Annotations-Separator" })));
31
31
  });
32
- return React.createElement("div", null, annotations);
32
+ return React.createElement("div", { className: "jgis-scrollable" }, annotations);
33
33
  }
34
34
  }
35
35
  export class Annotations extends PanelWithToolbar {
36
36
  constructor(options) {
37
37
  super({});
38
38
  this.title.label = 'Annotations';
39
- this.addClass('jGIS-Annotations');
39
+ this.addClass('jgis-scrollable');
40
40
  this._annotationModel = options.annotationModel;
41
41
  this._rightPanelModel = options.rightPanelModel;
42
42
  this._widget = ReactWidget.create(React.createElement(AnnotationsPanel, { rightPanelModel: this._rightPanelModel, annotationModel: this._annotationModel }));
@@ -1,6 +1,8 @@
1
1
  import { LabIcon, ReactWidget, caretDownIcon } from '@jupyterlab/ui-components';
2
2
  import { Panel } from '@lumino/widgets';
3
3
  import React, { useEffect, useRef, useState } from 'react';
4
+ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
5
+ import { faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons';
4
6
  export class IdentifyPanel extends Panel {
5
7
  constructor(options) {
6
8
  super();
@@ -9,6 +11,7 @@ export class IdentifyPanel extends Panel {
9
11
  this.id = 'jupytergis::identifyPanel';
10
12
  this.title.caption = 'Identify';
11
13
  this.title.label = 'Identify';
14
+ this.addClass('jgis-scrollable');
12
15
  this.addWidget(ReactWidget.create(React.createElement(IdentifyPanelComponent, { controlPanelModel: this._model, tracker: this._tracker })));
13
16
  }
14
17
  }
@@ -80,8 +83,13 @@ const IdentifyPanelComponent = ({ controlPanelModel, tracker }) => {
80
83
  jgisModel === null || jgisModel === void 0 ? void 0 : jgisModel.clientStateChanged.disconnect(handleClientStateChanged);
81
84
  };
82
85
  }, [jgisModel]);
86
+ const highlightFeatureOnMap = (feature) => {
87
+ var _a, _b;
88
+ (_a = jgisModel === null || jgisModel === void 0 ? void 0 : jgisModel.highlightFeatureSignal) === null || _a === void 0 ? void 0 : _a.emit(feature);
89
+ const geometry = feature.geometry || feature._geometry;
90
+ (_b = jgisModel === null || jgisModel === void 0 ? void 0 : jgisModel.flyToGeometrySignal) === null || _b === void 0 ? void 0 : _b.emit(geometry);
91
+ };
83
92
  const toggleFeatureVisibility = (index) => {
84
- console.log('visibleFeatures', visibleFeatures);
85
93
  setVisibleFeatures(prev => (Object.assign(Object.assign({}, prev), { [index]: !prev[index] })));
86
94
  };
87
95
  return (React.createElement("div", { className: "jgis-identify-wrapper", style: {
@@ -90,14 +98,28 @@ const IdentifyPanelComponent = ({ controlPanelModel, tracker }) => {
90
98
  : 'unset'
91
99
  } }, features &&
92
100
  Object.values(features).map((feature, featureIndex) => (React.createElement("div", { key: featureIndex, className: "jgis-identify-grid-item" },
93
- React.createElement("div", { className: "jgis-identify-grid-item-header", onClick: () => toggleFeatureVisibility(featureIndex) },
94
- React.createElement(LabIcon.resolveReact, { icon: caretDownIcon, className: `jp-gis-layerGroupCollapser${visibleFeatures[featureIndex] ? ' jp-mod-expanded' : ''}`, tag: 'span' }),
95
- React.createElement("span", null,
96
- "Feature ",
97
- featureIndex + 1,
98
- ":")),
101
+ React.createElement("div", { className: "jgis-identify-grid-item-header" },
102
+ React.createElement("span", { onClick: () => toggleFeatureVisibility(featureIndex) },
103
+ React.createElement(LabIcon.resolveReact, { icon: caretDownIcon, className: `jp-gis-layerGroupCollapser${visibleFeatures[featureIndex] ? ' jp-mod-expanded' : ''}`, tag: 'span' }),
104
+ React.createElement("span", null,
105
+ "Feature ",
106
+ featureIndex + 1)),
107
+ (() => {
108
+ const isRasterFeature = !feature.geometry &&
109
+ !feature._geometry &&
110
+ typeof (feature === null || feature === void 0 ? void 0 : feature.x) !== 'number' &&
111
+ typeof (feature === null || feature === void 0 ? void 0 : feature.y) !== 'number';
112
+ return (React.createElement("button", { className: "jgis-highlight-button", onClick: e => {
113
+ e.stopPropagation();
114
+ highlightFeatureOnMap(feature);
115
+ }, title: isRasterFeature
116
+ ? 'Highlight not available for raster features'
117
+ : 'Highlight feature on map', disabled: isRasterFeature },
118
+ React.createElement(FontAwesomeIcon, { icon: faMagnifyingGlass })));
119
+ })()),
99
120
  visibleFeatures[featureIndex] && (React.createElement(React.Fragment, null, Object.entries(feature)
100
121
  .filter(([key, value]) => typeof value !== 'object' || value === null)
122
+ .sort(([keyA], [keyB]) => keyA.localeCompare(keyB))
101
123
  .map(([key, value]) => (React.createElement("div", { key: key, className: "jgis-identify-grid-body" },
102
124
  React.createElement("strong", null,
103
125
  key,
@@ -94,6 +94,7 @@ function LayersBodyComponent(props) {
94
94
  };
95
95
  model === null || model === void 0 ? void 0 : model.sharedModel.layersChanged.connect(updateLayers);
96
96
  model === null || model === void 0 ? void 0 : model.sharedModel.layerTreeChanged.connect(updateLayers);
97
+ updateLayers();
97
98
  return () => {
98
99
  model === null || model === void 0 ? void 0 : model.sharedModel.layersChanged.disconnect(updateLayers);
99
100
  model === null || model === void 0 ? void 0 : model.sharedModel.layerTreeChanged.disconnect(updateLayers);
@@ -30,6 +30,7 @@ export class RightPanelWidget extends SidePanel {
30
30
  });
31
31
  identifyPanel.title.caption = 'Identify';
32
32
  identifyPanel.title.label = 'Identify';
33
+ identifyPanel.addClass('jgis-scrollable');
33
34
  this.addWidget(identifyPanel);
34
35
  this._model.documentChanged.connect((_, changed) => {
35
36
  if (changed) {
@@ -1,9 +1,9 @@
1
1
  import { CommandToolbarButton } from '@jupyterlab/apputils';
2
- import { MenuSvg, ReactWidget, ReactiveToolbar, ToolbarButton, addIcon, redoIcon, terminalIcon, undoIcon } from '@jupyterlab/ui-components';
2
+ import { MenuSvg, ReactWidget, ReactiveToolbar, ToolbarButton, addIcon, redoIcon, undoIcon } from '@jupyterlab/ui-components';
3
3
  import { Menu, Widget } from '@lumino/widgets';
4
4
  import * as React from 'react';
5
5
  import { CommandIDs } from '../constants';
6
- import { rasterIcon } from '../icons';
6
+ import { rasterIcon, terminalToolbarIcon } from '../icons';
7
7
  import { UsersItem } from './usertoolbaritem';
8
8
  export const TOOLBAR_SEPARATOR_CLASS = 'jGIS-Toolbar-Separator';
9
9
  export const TOOLBAR_GROUPNAME_CLASS = 'jGIS-Toolbar-GroupName';
@@ -21,41 +21,58 @@ export class ToolbarWidget extends ReactiveToolbar {
21
21
  super();
22
22
  this.addClass('jGIS-toolbar-widget');
23
23
  if (options.commands) {
24
- this.addItem('undo', new CommandToolbarButton({
24
+ const undoButton = new CommandToolbarButton({
25
25
  id: CommandIDs.undo,
26
26
  label: '',
27
27
  icon: undoIcon,
28
28
  commands: options.commands
29
- }));
30
- this.addItem('redo', new CommandToolbarButton({
29
+ });
30
+ this.addItem('undo', undoButton);
31
+ undoButton.node.dataset.testid = 'undo-button';
32
+ const redoButton = new CommandToolbarButton({
31
33
  id: CommandIDs.redo,
32
34
  label: '',
33
35
  icon: redoIcon,
34
36
  commands: options.commands
35
- }));
37
+ });
38
+ this.addItem('redo', redoButton);
36
39
  this.addItem('separator0', new Separator());
37
- this.addItem('Toggle console', new CommandToolbarButton({
40
+ const toggleConsoleButton = new CommandToolbarButton({
38
41
  id: CommandIDs.toggleConsole,
39
42
  commands: options.commands,
40
43
  label: '',
41
- icon: terminalIcon
42
- }));
44
+ icon: terminalToolbarIcon
45
+ });
46
+ this.addItem('Toggle console', toggleConsoleButton);
47
+ toggleConsoleButton.node.dataset.testid = 'toggle-console-button';
43
48
  this.addItem('separator1', new Separator());
44
- this.addItem('openLayerBrowser', new CommandToolbarButton({
49
+ const openLayersBrowserButton = new CommandToolbarButton({
45
50
  id: CommandIDs.openLayerBrowser,
46
51
  label: '',
47
52
  commands: options.commands
48
- }));
49
- this.addItem('newRasterEntry', new CommandToolbarButton({
53
+ });
54
+ this.addItem('openLayerBrowser', openLayersBrowserButton);
55
+ openLayersBrowserButton.node.dataset.testid = 'open-layers-browser';
56
+ const newRasterEntryButton = new CommandToolbarButton({
50
57
  id: CommandIDs.newRasterEntry,
51
58
  label: '',
52
59
  commands: options.commands
53
- }));
54
- this.addItem('newVectorTileEntry', new CommandToolbarButton({
60
+ });
61
+ this.addItem('newRasterEntry', newRasterEntryButton);
62
+ const newVectorTileEntryButton = new CommandToolbarButton({
55
63
  id: CommandIDs.newVectorTileEntry,
56
64
  label: '',
57
65
  commands: options.commands
58
- }));
66
+ });
67
+ this.addItem('newVectorTileEntry', newVectorTileEntryButton);
68
+ newRasterEntryButton.node.dataset.testid = 'new-raster-entry-button';
69
+ const geolocationButton = new CommandToolbarButton({
70
+ id: CommandIDs.getGeolocation,
71
+ commands: options.commands,
72
+ label: ''
73
+ });
74
+ this.addItem('Geolocation', geolocationButton);
75
+ geolocationButton.node.dataset.testid = 'geolocation-button';
59
76
  // vector sub menu
60
77
  const vectorSubMenu = new Menu({ commands: options.commands });
61
78
  vectorSubMenu.title.label = 'Add Vector Layer';
@@ -101,18 +118,24 @@ export class ToolbarWidget extends ReactiveToolbar {
101
118
  NewSubMenu.open(bbox.x, bbox.bottom);
102
119
  }
103
120
  });
121
+ NewEntryButton.node.dataset.testid = 'new-entry-button';
104
122
  this.addItem('New', NewEntryButton);
105
123
  this.addItem('separator2', new Separator());
106
- this.addItem('identify', new CommandToolbarButton({
124
+ const identifyButton = new CommandToolbarButton({
107
125
  id: CommandIDs.identify,
108
126
  label: '',
109
127
  commands: options.commands
110
- }));
111
- this.addItem('temporalController', new CommandToolbarButton({
128
+ });
129
+ this.addItem('identify', identifyButton);
130
+ identifyButton.node.dataset.testid = 'identify-button';
131
+ const temporalControllerButton = new CommandToolbarButton({
112
132
  id: CommandIDs.temporalController,
113
133
  label: '',
114
134
  commands: options.commands
115
- }));
135
+ });
136
+ this.addItem('temporalController', temporalControllerButton);
137
+ temporalControllerButton.node.dataset.testid =
138
+ 'temporal-controller-button';
116
139
  this.addItem('spacer', ReactiveToolbar.createSpacerItem());
117
140
  // Users
118
141
  this.addItem('users', ReactWidget.create(React.createElement(UsersItem, { model: options.model })));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jupytergis/base",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
4
4
  "description": "A JupyterLab extension for 3D modelling.",
5
5
  "keywords": [
6
6
  "jupyter",
@@ -43,7 +43,7 @@
43
43
  "@fortawesome/react-fontawesome": "latest",
44
44
  "@jupyter/react-components": "^0.16.6",
45
45
  "@jupyter/ydoc": "^2.0.0 || ^3.0.0",
46
- "@jupytergis/schema": "^0.4.4",
46
+ "@jupytergis/schema": "^0.4.5",
47
47
  "@jupyterlab/application": "^4.3.0",
48
48
  "@jupyterlab/apputils": "^4.3.0",
49
49
  "@jupyterlab/completer": "^4.3.0",
@@ -0,0 +1,19 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+ <svg
3
+ viewBox="0 0 576 512"
4
+ version="1.1"
5
+ id="svg1"
6
+ xmlns="http://www.w3.org/2000/svg"
7
+ xmlns:svg="http://www.w3.org/2000/svg">
8
+ <defs
9
+ id="defs1" />
10
+ <!--!Font
11
+ Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License -
12
+ https://fontawesome.com/license/free -->
13
+ <path
14
+ id="path1"
15
+ d="M542.2 32.1c-54.8 3.1-163.7 14.4-231 55.6-4.6 2.8-7.3 7.9-7.3 13.2v363.9c0 11.6 12.6 18.9 23.3 13.5 69.2-34.8 169.2-44.3 218.7-46.9 16.9-.9 30-14.4 30-30.7V62.8c0-17.7-15.4-31.7-33.8-30.7zM264.7 87.6C197.5 46.5 88.6 35.2 33.8 32.1 15.4 31 0 45 0 62.8V400.6c0 16.2 13.1 29.8 30 30.7 49.5 2.6 149.6 12.1 218.8 47 10.6 5.4 23.2-1.9 23.2-13.5V100.6c0-5.3-2.6-10.1-7.3-13z"
16
+ fill="#616161"
17
+ class="jp-icon3"
18
+ />
19
+ </svg>
@@ -0,0 +1,17 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+ <svg
3
+ viewBox="0 0 512 512"
4
+ version="1.1"
5
+ id="svg1"
6
+ xmlns="http://www.w3.org/2000/svg"
7
+ xmlns:svg="http://www.w3.org/2000/svg">
8
+ <defs
9
+ id="defs1" />
10
+ <!--!FontAwesome Free 6.7.2 by @fontawesome - https://fontawesome.com License -https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
11
+ <path
12
+ d="M 256,47.68 C 140.92,47.68 47.68,140.92 47.68,256 47.68,371.08 140.92,464.32 256,464.32 371.08,464.32 464.32,371.08 464.32,256 464.32,140.92 371.08,47.68 256,47.68 Z m 77.7,262.92 v 0 l -16.8,21 a 13.44,13.44 0 0 1 -18.9,2.1 v 0 l -56.28,-41.748 a 33.6,33.6 0 0 1 -12.6,-26.208 V 135.04 a 13.44,13.44 0 0 1 13.44,-13.44 h 26.88 a 13.44,13.44 0 0 1 13.44,13.44 V 256 l 48.72,35.7 a 13.44,13.44 0 0 1 2.1,18.9 z"
13
+ id="path1"
14
+ style="stroke-width:0.84"
15
+ fill="#616161"
16
+ class="jp-icon3" />
17
+ </svg>
@@ -0,0 +1,15 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+ <svg
3
+ viewBox="0 0 384 512"
4
+ version="1.1"
5
+ id="svg1"
6
+ xmlns="http://www.w3.org/2000/svg"
7
+ xmlns:svg="http://www.w3.org/2000/svg">
8
+ <defs
9
+ id="defs1" />
10
+ <!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
11
+ <path
12
+ d="M172.3 501.7C27 291 0 269.4 0 192 0 86 86 0 192 0s192 86 192 192c0 77.4-27 99-172.3 309.7-9.5 13.8-29.9 13.8-39.5 0zM192 272c44.2 0 80-35.8 80-80s-35.8-80-80-80-80 35.8-80 80 35.8 80 80 80z"
13
+ id="path1"
14
+ style="fill:#616161;fill-opacity:1" />
15
+ </svg>
@@ -0,0 +1,20 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+ <svg
3
+ viewBox="0 0 192 512"
4
+ version="1.1"
5
+ id="svg1"
6
+ xmlns="http://www.w3.org/2000/svg"
7
+ xmlns:svg="http://www.w3.org/2000/svg">
8
+ <defs
9
+ id="defs1" />
10
+ <!--!Font
11
+ Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License -
12
+ https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
13
+ <path
14
+ d="m 35.2,390.56 h 16 V 275.04 h -16 c -8.8,0 -16,-7.2 -16,-16 V 220.8 c 0,-8.8 7.2,-16 16,-16 h 89.6 c 8.8,0 16,7.2 16,16 v 169.76 h 16 c 8.8,0 16,7.2 16,16 v 38.24 c 0,8.8 -7.2,16 -16,16 H 35.2 c -8.8,0 -16,-7.2 -16,-16 v -38.24 c 0,-8.8 7.2,-16 16,-16 z M 96,51.2 c -31.84,0 -57.6,25.76 -57.6,57.6 0,31.84 25.76,57.6 57.6,57.6 31.84,0 57.6,-25.76 57.6,-57.6 0,-31.84 -25.76,-57.6 -57.6,-57.6 z"
15
+ id="path1"
16
+ style="stroke-width:0.8"
17
+ fill="#616161"
18
+ class="jp-icon3"
19
+ />
20
+ </svg>
@@ -1,5 +1,6 @@
1
1
  <svg height="16" viewBox="0 0 24 24" width="16" xmlns="http://www.w3.org/2000/svg">
2
- <g class="jp-icon2 jp-icon-selectable" fill="#e2e2e2">
3
- <path d="M 22,8 H 2 v 1 h 20 z m 0,10.5 H 2 v 1 H 22 Z M 22,15 H 2 v 1 h 20 z m 0,-3.5 H 2 v 1 h 20 z m 0,-7 H 2 v 1 H 22 Z M 5.5,2 h -1 v 20 h 1 z M 9,2 H 8 v 20 h 1 z m 3.5,0 h -1 v 20 h 1 z M 16,2 h -1 v 20 h 1 z m 3.5,0 h -1 v 20 h 1 z"/>
2
+ <g class="jp-icon3 jp-icon-selectable" fill="#616161">
3
+ <path
4
+ d="M 22,8 H 2 v 1 h 20 z m 0,10.5 H 2 v 1 H 22 Z M 22,15 H 2 v 1 h 20 z m 0,-3.5 H 2 v 1 h 20 z m 0,-7 H 2 v 1 H 22 Z M 5.5,2 h -1 v 20 h 1 z M 9,2 H 8 v 20 h 1 z m 3.5,0 h -1 v 20 h 1 z M 16,2 h -1 v 20 h 1 z m 3.5,0 h -1 v 20 h 1 z" />
4
5
  </g>
5
6
  </svg>
@@ -0,0 +1,9 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font
2
+ Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License -
3
+ https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
4
+ <path
5
+ d="M256 0c17.7 0 32 14.3 32 32l0 34.7C368.4 80.1 431.9 143.6 445.3 224l34.7 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-34.7 0C431.9 368.4 368.4 431.9 288 445.3l0 34.7c0 17.7-14.3 32-32 32s-32-14.3-32-32l0-34.7C143.6 431.9 80.1 368.4 66.7 288L32 288c-17.7 0-32-14.3-32-32s14.3-32 32-32l34.7 0C80.1 143.6 143.6 80.1 224 66.7L224 32c0-17.7 14.3-32 32-32zM128 256a128 128 0 1 0 256 0 128 128 0 1 0 -256 0zm128-80a80 80 0 1 1 0 160 80 80 0 1 1 0-160z"
6
+ fill="#616161"
7
+ class="jp-icon3"
8
+ />
9
+ </svg>
@@ -0,0 +1,17 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+ <svg
3
+ viewBox="0 0 512 512"
4
+ version="1.1"
5
+ id="svg1"
6
+ xmlns="http://www.w3.org/2000/svg"
7
+ xmlns:svg="http://www.w3.org/2000/svg">
8
+ <defs
9
+ id="defs1" />
10
+ <!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
11
+ <path
12
+ d="m 256,0 c 17.7,0 32,14.3 32,32 V 66.7 C 368.4,80.1 431.9,143.6 445.3,224 H 480 c 17.7,0 32,14.3 32,32 0,17.7 -14.3,32 -32,32 H 445.3 C 431.9,368.4 368.4,431.9 288,445.3 V 480 c 0,17.7 -14.3,32 -32,32 -17.7,0 -32,-14.3 -32,-32 V 445.3 C 143.6,431.9 80.1,368.4 66.7,288 H 32 C 14.3,288 0,273.7 0,256 0,238.3 14.3,224 32,224 H 66.7 C 80.1,143.6 143.6,80.1 224,66.7 V 32 C 224,14.3 238.3,0 256,0 Z M 128,256 c 0,70.6925 57.30755,127.99994 128,127.99994 70.69245,0 128,-57.30744 128,-127.99994 0,-70.6925 -57.30755,-127.99994 -128,-127.99994 -70.69245,0 -128,57.30744 -128,127.99994 z"
13
+ id="path1"
14
+ fill="#616161"
15
+ class="jp-icon3"
16
+ />
17
+ </svg>
@@ -0,0 +1,12 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" viewBox="0 0 24 24">
2
+ <path
3
+ d="M2 2h20v20H2z"
4
+ fill="#616161"
5
+ class="jp-icon3"
6
+ />
7
+ <path
8
+ d="M9.01 14.762q0-.246-.077-.434a.9.9 0 0 0-.234-.351 1.6 1.6 0 0 0-.422-.288 5 5 0 0 0-.627-.263q-.592-.211-1.078-.446a3.5 3.5 0 0 1-.832-.544 2.2 2.2 0 0 1-.528-.721 2.4 2.4 0 0 1-.187-.985q0-.498.17-.908a2.1 2.1 0 0 1 .48-.72q.31-.306.75-.493.44-.188.979-.24V7.11h.937v1.272q.527.07.95.287.421.217.714.568.3.345.457.82.165.47.164 1.055H8.998q0-.709-.323-1.072-.322-.37-.873-.37-.299 0-.521.083a.9.9 0 0 0-.358.223.9.9 0 0 0-.21.334q-.066.194-.065.421 0 .23.064.41a.9.9 0 0 0 .229.329q.165.152.428.293.263.134.656.275.591.223 1.072.463.48.235.82.55.346.312.528.727.187.41.187.973 0 .515-.17.932-.17.41-.486.709t-.762.48a3.7 3.7 0 0 1-.996.229v1.148h-.931V17.1a4 4 0 0 1-.967-.217 2.6 2.6 0 0 1-.832-.504 2.4 2.4 0 0 1-.574-.826q-.217-.505-.217-1.207h1.635q0 .421.123.709.123.281.316.45.2.165.451.235.252.07.516.07.627 0 .949-.292a.98.98 0 0 0 .322-.756m8.36 3.51h-5.343V17h5.344z"
9
+ fill="#bdbdbd"
10
+ class="jp-icon-accent1"
11
+ />
12
+ </svg>
@@ -0,0 +1,21 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+ <svg
3
+ class="jp-icon3"
4
+ viewBox="0 0 512 512"
5
+ version="1.1"
6
+ id="svg1"
7
+ xmlns="http://www.w3.org/2000/svg"
8
+ xmlns:svg="http://www.w3.org/2000/svg">
9
+ <defs
10
+ id="defs1" />
11
+ <!--!Font
12
+ Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License -
13
+ https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
14
+ <path
15
+ d="M 486.4,140.8 V 54.4 c 0,-15.93 -12.87,-28.8 -28.8,-28.8 h -86.4 c -15.93,0 -28.8,12.87 -28.8,28.8 H 169.6 c 0,-15.93 -12.87,-28.8 -28.8,-28.8 H 54.4 c -15.93,0 -28.8,12.87 -28.8,28.8 v 86.4 c 0,15.93 12.87,28.8 28.8,28.8 v 172.8 c -15.93,0 -28.8,12.87 -28.8,28.8 v 86.4 c 0,15.93 12.87,28.8 28.8,28.8 h 86.4 c 15.93,0 28.8,-12.87 28.8,-28.8 h 172.8 c 0,15.93 12.87,28.8 28.8,28.8 h 86.4 c 15.93,0 28.8,-12.87 28.8,-28.8 v -86.4 c 0,-15.93 -12.87,-28.8 -28.8,-28.8 V 169.6 c 15.93,0 28.8,-12.87 28.8,-28.8 z M 400,83.2 h 28.8 V 112 H 400 Z m -316.8,0 H 112 V 112 H 83.2 Z M 112,428.8 H 83.2 V 400 H 112 Z m 316.8,0 H 400 V 400 h 28.8 z M 400,342.4 h -28.8 c -15.93,0 -28.8,12.87 -28.8,28.8 V 400 H 169.6 v -28.8 c 0,-15.93 -12.87,-28.8 -28.8,-28.8 H 112 V 169.6 h 28.8 c 15.93,0 28.8,-12.87 28.8,-28.8 V 112 h 172.8 v 28.8 c 0,15.93 12.87,28.8 28.8,28.8 H 400 Z"
16
+ id="path1"
17
+ style="stroke-width:0.9"
18
+ fill="#616161"
19
+ class="jp-icon3"
20
+ />
21
+ </svg>