Source Toc
Source Toc
Cette page est générée automatiquement à partir du dépôt local au moment de la génération de la documentation.
Fichiers inclus
packages/common/src/lib/widgets/toc/toc.declaration.tspackages/common/src/lib/widgets/toc/toc.config.tspackages/common/src/lib/widgets/toc/components/ExportButton.sveltepackages/common/src/lib/widgets/toc/components/ScaleMessage.sveltepackages/common/src/lib/widgets/toc/components/TimeTravelController.sveltepackages/common/src/lib/widgets/toc/components/TimeTravelIcon.sveltepackages/common/src/lib/widgets/toc/components/TocActionIcon.sveltepackages/common/src/lib/widgets/toc/components/TocBasemapSelector.sveltepackages/common/src/lib/widgets/toc/components/TocItem.sveltepackages/common/src/lib/widgets/toc/components/TocLayerActions.sveltepackages/common/src/lib/widgets/toc/components/TocLayerIcon.sveltepackages/common/src/lib/widgets/toc/components/ZoomToExtentButton.sveltepackages/common/src/lib/widgets/toc/toc.i18n.tspackages/common/src/lib/widgets/toc/toc.state.svelte.tspackages/common/src/lib/widgets/toc/Toc.sveltepackages/common/src/lib/widgets/toc/toc.topic.tspackages/common/src/lib/widgets/toc/toc.utils.ts
packages/common/src/lib/widgets/toc/toc.declaration.ts
import { widgetFactorySvelte, type WidgetProps } from '$lib/api/managers/widget';import type { WidgetDeclaration } from '$lib/api/managers/widget/widget-declaration';import { TocState } from './toc.state.svelte';import { type TocFullConfig, tocFullConfigSchema } from './toc.config';
export const declaration = { factory: () => import('./Toc.svelte').then((Toc) => widgetFactorySvelte(Toc)), schema: () => tocFullConfigSchema, state: (props) => new TocState(props),} satisfies WidgetDeclaration;
export type TocProps = WidgetProps<TocFullConfig, TocState>;packages/common/src/lib/widgets/toc/toc.config.ts
import { defineWidgetConfig } from '$lib/api/managers/configuration/models/widget/widget-configuration.schema';import { inToolbarSchemaFrom } from '$lib/api/managers/configuration/models/widget/widget-in-toolbar.schema';import { i18nSchemaFrom } from '$lib/api/managers/i18n/i18n.schema';import { z } from 'zod';import { tocI18n } from '$lib/widgets/toc/toc.i18n';
export const timeTravelControllerConfig = z.object({ showMetadataUrlButton: z.boolean().default(true), autoPlayTransitionIsSeconds: z.number().default(2),});export type TimeTravelControllerConfig = z.infer<typeof timeTravelControllerConfig>;export const tocConfigSchema = z.object({ dataTableWidgetId: z.string().optional(), addDataWidgetId: z.string().default('addDataWidgetId'), drawWidgetId: z.string().default('draw'), showAddDataButton: z.boolean().default(true), showClearAllButton: z.boolean().default(true), showBasemapSelector: z.boolean().default(true), showLayersLoadingState: z.boolean().default(true), timeTravelControllerConfig: timeTravelControllerConfig.prefault({}), maxLengthForServiceDescription: z.number().default(150),});
export const tocFullConfigSchema = defineWidgetConfig({ title: tocI18n.title, icon: { lucide: 'Layers', }, inToolbar: inToolbarSchemaFrom({ type: 'button', }), i18n: i18nSchemaFrom(tocI18n), config: tocConfigSchema.prefault({}),});
export type TocFullConfig = z.infer<typeof tocFullConfigSchema>;export type TocConfig = z.infer<typeof tocConfigSchema>;export type TocI18n = TocFullConfig['i18n'];packages/common/src/lib/widgets/toc/components/ExportButton.svelte
<script lang="ts"> import type { ApiGraphicsMapService } from '$lib/api/mapservices'; import ExportDialog from '$lib/components/common/ExportDialog.svelte'; import TocActionIcon from './TocActionIcon.svelte'; import type { HTMLButtonAttributes } from 'svelte/elements';
interface Props extends HTMLButtonAttributes { active?: boolean; service: ApiGraphicsMapService; parentVisible: boolean; }
let { active = false, service, parentVisible, ...props }: Props = $props();</script>
<ExportDialog exportFileName={service.label} features={service.features}> <TocActionIcon {...props} {active} icon={{ lucide: 'ExternalLink' }} visibleOnMap={service.visible && parentVisible} /></ExportDialog>packages/common/src/lib/widgets/toc/components/ScaleMessage.svelte
<script lang="ts"> import type { ApiMapService } from '$lib/api/mapservices'; import type { ApiSublayer } from '$lib/api/layers'; import { isMapService, isVisibleAtScale } from '$lib/api/utils/service.utils'; import { getMapManager } from '$lib/api/map'; import { onDestroy } from 'svelte'; import { getI18n } from '$lib/api/managers/i18n'; import type { TocI18n } from '$lib/widgets/toc/toc.config'; import { cn } from '$lib/components/shadcn/utils';
interface Props { service: ApiMapService | ApiSublayer; visibleAtCurrentScale: boolean; i18n: TocI18n; class?: string; } let { service, i18n: tocI18n, visibleAtCurrentScale = $bindable(false), class: className }: Props = $props();
const mapManager = getMapManager(); const scale = mapManager.tools.scale; const i18n = getI18n(tocI18n);
let currentScale = $state<number | undefined>();
const message = $derived.by(() => { if (!currentScale) return ''; const minScale = service.minScale; const maxScale = service.maxScale; if (minScale && maxScale && (minScale < currentScale || maxScale > currentScale)) { return i18n('zoom-in-or-out', { minScale, maxScale }); } if (minScale && minScale < currentScale) { return i18n('visible-under', { minScale }); } else if (maxScale && maxScale > currentScale) { return i18n('visible-above', { maxScale }); } return ''; });
const scaleWatchUnsubscriber = mapManager.tools.events.watch( 'SCALE', (newScale) => { currentScale = newScale; visibleAtCurrentScale = isVisibleAtScale(service, newScale); }, { initial: true }, );
$effect(() => { if (isMapService(service)) { service.onLoad(() => { const initScale = scale.getCurrentScaleInfo()?.scaleForDevice; if (initScale) { visibleAtCurrentScale = isVisibleAtScale(service, initScale); } }); } });
onDestroy(() => { scaleWatchUnsubscriber(); });</script>
{#if !visibleAtCurrentScale} <span class={cn('gv-text-sm gv-text-heading', className)}>{message}</span>{/if}packages/common/src/lib/widgets/toc/components/TimeTravelController.svelte
<script lang="ts"> import { type ApiTimeTravellable } from '$lib/api/mapservices'; import { cn } from '$lib/components/shadcn/utils'; import type { ApiMapServiceWithImageUrl } from '$lib/api/managers/configuration'; import { ChevronLeft } from 'lucide-svelte'; import ChevronRight from 'lucide-svelte/icons/chevron-right'; import { buttonVariants } from '$lib/components/shadcn/ui/button'; import { getI18n } from '$lib/api/managers/i18n'; import type { TimeTravelControllerConfig, TocI18n } from '$lib/widgets/toc/toc.config';
interface Props { service: ApiTimeTravellable; config: TimeTravelControllerConfig; i18n: TocI18n; thumbnailIndex: number; class?: string; }
type MapserviceWithDynamicThumbnails = ApiMapServiceWithImageUrl & { thumbnailUrl?: string };
let { service, config, i18n: i18nConfig, class: className, thumbnailIndex = $bindable() }: Props = $props();
const i18n = getI18n(i18nConfig); const numberOfVisibleElementAtATime = service.numberOfVisibleTiles;
const currentIndex = $derived.by(() => (service.selected ? service.servicesConfig.indexOf(service.selected) : 0)); const totalElements = $derived.by(() => service.servicesConfig.length); const mapServicesConfig: MapserviceWithDynamicThumbnails[] = $derived.by(() => { return service.servicesConfig.map((cfg) => ({ ...cfg, thumbnailUrl: cfg.imageUrl ?? `${cfg.url}/info/thumbnail`, })); });
const visiblePages = $derived.by(() => { const maxPageToShow = numberOfVisibleElementAtATime; if (mapServicesConfig.length <= maxPageToShow) { return Array.from({ length: mapServicesConfig.length }, (_, i) => i); } return Array.from({ length: maxPageToShow }, (_, i) => thumbnailIndex + i); });
$effect(() => { const half = Math.trunc(numberOfVisibleElementAtATime / 2); if ( currentIndex >= half && currentIndex < totalElements - (numberOfVisibleElementAtATime - 1 - half) && currentIndex > 0 ) { thumbnailIndex = currentIndex - half; } });
function selectMapService(mapService: ApiMapServiceWithImageUrl): void { service.select(mapService); }
function previousImage() { if (thumbnailIndex > 0) { thumbnailIndex -= 1; } }
function nextImage() { if (thumbnailIndex < totalElements - numberOfVisibleElementAtATime) { thumbnailIndex += 1; } }
function isSelected(mapServiceConfig: ApiMapServiceWithImageUrl) { return mapServiceConfig.id === service.selected?.id; }
function handleKeyDown(event: KeyboardEvent) { if (event.key === 'ArrowRight') { nextImage(); event.stopPropagation(); } else if (event.key === 'ArrowLeft') { previousImage(); event.stopPropagation(); } }</script>
<div role="button" tabindex="0" onkeydown={(evt) => handleKeyDown(evt)} class={cn('gv-space-y-2.5', className)}> <div class="gv-relative gv-w-full gv-flex gv-items-center gv-gap-1"> {#if thumbnailIndex > 0} <div class="gv-bg-gradient-to-l gv-from-background/0 gv-to-background gv-absolute gv-left-0 gv-w-20 gv-h-full gv-z-10 gv-pointer-events-none" > <button onclick={previousImage} class={cn( 'gv-absolute gv-left-0 gv-top-1/2 gv-transform gv--translate-y-1/2 gv-z-15 gv-pointer-events-auto', )} data-test-id="TimeTravel-PreviousPage" > <ChevronLeft /> </button> </div> {/if}
<div class="gv-flex gv-flex-1 gv-overflow-hidden gv-gap-0.5 gv-relative"> <!-- Handle service load error (in red and not clickable ?)--> {#each visiblePages as pageIndex (pageIndex)} {@const config = mapServicesConfig[pageIndex]} <button title={config.label} onclick={() => selectMapService(config)} class={cn( 'gv-flex-1 gv-text-center gv-relative', isSelected(config) ? 'gv-border-solid gv-border-2 gv-border-primary' : 'gv-opacity-50 gv-mt-2', )} data-test-id={`TimeTravel-${config.id}`} data-test-element-index={pageIndex} > <img src={config.thumbnailUrl} alt="Thumbnail of map service" class="gv-w-full gv-h-16 gv-object-cover" /> <span class={cn(isSelected(config) && 'gv-text-primary')}> {config.shortLabel} </span> </button> {/each} </div>
{#if thumbnailIndex < totalElements - numberOfVisibleElementAtATime} <div class="gv-bg-gradient-to-r gv-from-background/0 gv-to-background gv-absolute gv-right-0 gv-w-20 gv-h-full gv-z-10 gv-pointer-events-none" > <button onclick={nextImage} class={cn( 'gv-absolute gv-right-0 gv-top-1/2 gv-transform gv--translate-y-1/2 gv-z-15 gv-pointer-events-auto', )} data-test-id="TimeTravel-NextPage" > <ChevronRight /> </button> </div> {/if} </div>
{#if config.showMetadataUrlButton && service.selected?.metadataUrl} <div class="gv-flex gv-justify-start"> <a href={service.selected.metadataUrl} target="_blank" rel="noopener noreferrer" class={buttonVariants({ variant: 'outline' })} data-test-id="TimeTravel-MetadataLink" > {i18n('view-complete-label', { label: service.selected.label })} </a> </div> {/if}</div>packages/common/src/lib/widgets/toc/components/TimeTravelIcon.svelte
<script lang="ts"> import type { ApiTimeTravellable } from '$lib/api/mapservices'; import type { TimeTravelControllerConfig } from '$lib/widgets/time-travel-controller/time-travel-controller.config'; import { onDestroy } from 'svelte'; import TocActionIcon from './TocActionIcon.svelte';
interface Props { service: ApiTimeTravellable; config: TimeTravelControllerConfig; onPlayClicked: () => void; }
let { service, config, onPlayClicked = () => {} }: Props = $props();
let isPlaying = $state<boolean>(false); let interval: NodeJS.Timeout | undefined;
function play() { onPlayClicked(); service.visible = true; if (!service.selected) return; isPlaying = true; const lastIndex = service.servicesConfig.length - 1; let currentIndex = service.servicesConfig.indexOf(service.selected); // Play from start if last service is currently selected if (lastIndex === currentIndex) { service.select(service.servicesConfig[0]); currentIndex = service.servicesConfig.indexOf(service.selected); } interval = setInterval(() => { if (currentIndex === lastIndex) { pause(); return; } currentIndex += 1; service.select(service.servicesConfig[currentIndex]); }, config.autoPlayTransitionIsSeconds * 1000); }
function pause() { isPlaying = false; if (interval) { clearInterval(interval); } }
onDestroy(() => { if (interval) { clearInterval(interval); } });</script>
{#if isPlaying} <TocActionIcon onclick={() => pause()} icon={{ lucide: 'Pause' }} visibleOnMap={service.visible} />{:else} <TocActionIcon onclick={() => play()} icon={{ lucide: 'Play' }} visibleOnMap={service.visible} />{/if}packages/common/src/lib/widgets/toc/components/TocActionIcon.svelte
<script lang="ts"> import type { Icon as IconSchemaType } from '$lib/api/icons'; import { Icon } from '$lib/components/icon'; import { cn } from '$lib/components/shadcn/utils'; import type { HTMLButtonAttributes } from 'svelte/elements'; import { ApiSimpleTooltip } from '$lib/components/api-simple-tooltip';
interface Props extends HTMLButtonAttributes { active?: boolean; visibleOnMap?: boolean; icon: IconSchemaType; iconClass?: string; tooltip?: string; } const { active, icon, visibleOnMap = true, class: className, iconClass, onclick, disabled, tooltip, ...restProps }: Props = $props(); const onButtonClick: HTMLButtonAttributes['onclick'] = (event) => { event.stopPropagation(); onclick?.(event); };</script>
<ApiSimpleTooltip text={tooltip}> <button {...restProps} onclick={onButtonClick} class={cn( 'gv-px-1.5 last:gv-pr-0 gv-border-r last:gv-border-none', visibleOnMap && active && 'gv-text-primary-300', visibleOnMap && !active && 'gv-text-primary-500 hover:gv-text-primary-300', !visibleOnMap && active && 'gv-text-heading hover:gv-text-black', !visibleOnMap && !active && 'gv-text-body hover:gv-text-black', disabled && 'gv-text-body gv-pointer-events-none', className, )} > <Icon {icon} class={cn('gv-size-4', iconClass)} /> </button></ApiSimpleTooltip>packages/common/src/lib/widgets/toc/components/TocBasemapSelector.svelte
<script lang="ts"> import { Icon } from '$lib/components/icon'; import { getMapManager } from '$lib/api/map'; import { Button } from '$lib/components/shadcn/ui/button'; import { getI18n } from '$lib/api/managers/i18n'; import type { TocI18n } from '$lib/widgets/toc/toc.config'; import { SquareArrowOutUpRight } from 'lucide-svelte'; import ChevronUp from 'lucide-svelte/icons/chevron-up'; import ChevronDown from 'lucide-svelte/icons/chevron-down'; import { cn } from '$lib/components/shadcn/utils'; import BasemapDetails from '$lib/widgets/basemap-chooser/BasemapDetails.svelte'; import { fly } from 'svelte/transition'; import type { ApiBasemap } from '$lib/api/mapservices'; import { calculateThumbnailUrl } from '$lib/api/utils'; import { getTopicManager } from '$lib/api/managers/topic';
interface Props { i18nRegistry: TocI18n; open: boolean; }
let { i18nRegistry, open = $bindable() }: Props = $props();
const i18n = getI18n(i18nRegistry); const mapManager = getMapManager(); const basemapList = mapManager.basemapList; const topic = getTopicManager();
const selectedBasemapThumbnail = $derived.by(() => basemapList.selected ? calculateThumbnailUrl(basemapList.selected) : undefined, );
let makeItAnimate = $state(true);
// 100% worth it $effect(() => { if (open) { makeItAnimate = false; setTimeout(() => { makeItAnimate = true; }); } });
function onBasemapClick(basemap: ApiBasemap) { basemapList.select(basemap); topic.publish({ type: 'BasemapChooser-select', basemap, source: 'Toc', }); }</script>
{#if basemapList.selected && makeItAnimate} <div in:fly={{ duration: 600, y: '100dvh' }} class="gv-border-t"> <button class="gv-w-full gv-h-20 gv-p-2 gv-flex hover:gv-bg-grey-50" onclick={() => (open = !open)} data-test-id="Toc-Basemap-Expand-Toggle" > <div class="gv-h-full gv-w-fit gv-aspect-square"> {#if basemapList.selected.icon} <Icon icon={basemapList.selected.icon} alt="basemap-image" /> {:else if selectedBasemapThumbnail} <img src={selectedBasemapThumbnail} alt="basemap thumbnail" /> {:else} <div class="gv-h-full gv-bg-muted"></div> {/if} </div> <div class="gv-h-full gv-w-full gv-flex-1 gv-flex gv-flex-col gv-justify-between gv-text-left gv-ml-2"> <span class="gv-font-bold">{i18n('basemap-title')}</span> <span class="gv-text-md gv-text-primary-500">{basemapList.selected.label}</span> <Button variant="outline" href={basemapList.selected.metadataUrl} target="_blank" onclick={(event: MouseEvent) => event.stopPropagation()} size="sm" class={cn('gv-w-fit', basemapList.selected.metadataUrl ? 'gv-visible' : 'gv-invisible')} data-test-id="Toc-Basemap-Link-Metadata" > {i18n('basemap-view-complete-data')} <SquareArrowOutUpRight class="gv-p-1" /> </Button> </div> {#if open} <ChevronDown /> {:else} <ChevronUp /> {/if} </button>
{#if open} <div> {#each basemapList.list as basemap (basemap.id)} {@const thumbnailUrl = calculateThumbnailUrl(basemap)} <button data-state={basemap.selected ? 'active' : ''} class="gv-h-14 gv-w-full gv-p-2 gv-flex gv-items-center gv-border-t data-active:gv-font-bold hover:gv-bg-grey-50" onclick={() => onBasemapClick(basemap)} data-test-id={`Toc-Basemap-Select-${basemap.id}`} > <div class={cn( 'gv-h-full gv-w-fit gv-aspect-square gv-border-2 gv-p-[2px] gv-border-transparent', basemap.selected ? 'gv-border-primary' : '', )} > {#if basemap.icon} <Icon icon={basemap.icon} alt="basemap-image" /> {:else if thumbnailUrl} <img class="gv-size-8" src={thumbnailUrl} alt="basemap thumbnail" /> {:else} <div class="gv-h-full gv-bg-muted"></div> {/if} </div> <span class={cn( 'gv-flex-1 gv-text-md gv-text-left gv-ml-3', !basemap.selected ? 'gv-text-primary' : '', )} > {basemap.label} </span> </button> {/each}
<div class="gv-p-2"> <BasemapDetails basemap={basemapList.selected} {i18nRegistry} defaultOpen={true} showMetaDataUrl={false} /> </div> </div> {/if} </div>{/if}packages/common/src/lib/widgets/toc/components/TocItem.svelte
<script lang="ts"> import type { ApiSublayer } from '$lib/api/layers/api-sublayer.svelte'; import { getI18n } from '$lib/api/managers/i18n'; import { type ApiMapService } from '$lib/api/mapservices'; import { type BaseListWrapper, truncate } from '$lib/api/utils'; import { LegendCacheItem } from '$lib/api/utils/legend/legend.model.svelte'; import { isGraphicService, isMapService, isTimeTravelService } from '$lib/api/utils/service.utils'; import Error from '$lib/components/common/Error.svelte'; import OpacitySlider from '$lib/components/opacity-slider/OpacitySlider.svelte'; import { Label } from '$lib/components/shadcn/ui/label'; import { cn } from '$lib/components/shadcn/utils'; import LegendItemDisplay from '$lib/widgets/legend/legend-list/LegendItemDisplay.svelte'; import { isMapServiceWithUrl } from '$lib/widgets/legend/models/legend-data.svelte'; import TocItem from './TocItem.svelte'; import Loader from 'lucide-svelte/icons/loader'; import ScaleMessage from './ScaleMessage.svelte'; import TimeTravelController from './TimeTravelController.svelte'; import { Sortable } from '$lib/components/sortable'; import TocLayerIcon from './TocLayerIcon.svelte'; import TocLayerActions from './TocLayerActions.svelte'; import { getTopicManager } from '$lib/api/managers/topic'; import type { TocConfig, TocI18n } from '../toc.config'; import { getState, type TocItemState } from '../toc.state.svelte'; import { ExternalLink } from 'lucide-svelte'; import { hasSublayers, isSublayer } from '$lib/api/layers'; import { Button } from '$lib/components/shadcn/ui/button'; import ServiceLoadingStatus from '$lib/components/service-loading-status/ServiceLoadingStatus.svelte';
interface Props { isRoot?: boolean; state: TocItemState; layerList: BaseListWrapper<ApiMapService | ApiSublayer>; i18nConfig: TocI18n; placeholder: string; parentVisible?: boolean; open?: boolean; highlight?: boolean; legendCacheItem?: LegendCacheItem; class?: string; headerClass?: string; dataTestIdPrefix?: string; config: TocConfig; headerElement?: HTMLDivElement; }
let { isRoot = false, state: itemState, layerList, i18nConfig, placeholder, highlight, class: className, headerClass, open = $bindable(false), parentVisible = true, legendCacheItem = undefined, dataTestIdPrefix, config, headerElement: thisHeaderElement = $bindable(), }: Props = $props();
const topic = getTopicManager(); const i18n = getI18n(i18nConfig); const tocState = getState();
let visibleAtCurrentScale = $state(true); let thumbnailIndex = $state<number>(0);
const service = $derived(itemState.service); const dataTestId = $derived(`${dataTestIdPrefix ?? ''}${i18n.translate(service.label)}`); const canExpand = $derived( (isMapService(service) && hasSublayers(service)) || isSublayer(service) || isTimeTravelService(service), );
if (!legendCacheItem && isMapServiceWithUrl(service)) { legendCacheItem = new LegendCacheItem(service); }
function onHeaderClick() { if (canExpand) { open = !open; } }
function onHeaderKeyDown(event: KeyboardEvent) { if (event.key === ' ') { onHeaderClick(); event.stopPropagation(); } }
function onMetadataUrlClick() { topic.publish({ type: 'Toc-Layer-metadata-open', layer: service as ApiMapService, }); }</script>
<div class={cn( 'gv-flex gv-flex-col gv-w-full gv-transition-colors', !service.visible && 'gv-bg-muted-500', highlight && 'gv-animate-blink', className, )}> <!--Layer expand toggle--> <!--TODO: Rename data-test-id--> <div bind:this={thisHeaderElement} class={cn( 'gv-group gv-px-5 gv-text-start gv-py-3.5 gv-transition-colors gv-border-2 gv-border-transparent', parentVisible && 'hover:gv-bg-primary-100', (!service.visible || !parentVisible) && 'hover:gv-bg-muted-600', !canExpand && 'gv-cursor-grab', isGraphicService(service) && service.editing && 'gv-border-2 gv-border-primary', headerClass, )} role="button" tabindex="0" onclick={onHeaderClick} onkeydown={onHeaderKeyDown} data-test-id={`${dataTestId}-Toc-Chevron`} > <!--Info row (icon - label - actions)--> <div class="gv-flex gv-gap-2 gv-items-center"> <!--Layer icon--> <TocLayerIcon {isRoot} {service} {open} {parentVisible} class={!visibleAtCurrentScale ? 'gv-opacity-50' : ''} />
<div class="gv-flex-1"> <!--Layer name--> <Label data-test-id={`${dataTestId}-Toc-label`} class={cn( 'gv-cursor-inherit gv-font-bold gv-duration-300 gv-text-wrap gv-whitespace-nowrap', isMapService(service) && 'gv-cursor-inherit', !visibleAtCurrentScale && 'gv-opacity-50', )} > {i18n.translate(service.label)} </Label>
<!--Graphic layer editing message--> {#if isGraphicService(service) && service.editing} <span class="gv-text-sm gv-text-primary-500 gv-font-bold gv-ml-2">{i18n('editing')}</span> {/if} </div>
{#if config.showLayersLoadingState} <ServiceLoadingStatus {service} /> {/if}
<!--Layer actions--> <TocLayerActions bind:showActions={itemState.showActions} bind:showInfo={itemState.showInfo} bind:showOpacity={itemState.showOpacity} {service} {dataTestId} {layerList} {parentVisible} {config} onPlayClicked={() => (open = true)} i18n={i18nConfig} /> </div>
<ScaleMessage i18n={i18nConfig} {service} bind:visibleAtCurrentScale class="gv-pl-6" /> </div>
<!--Opacity--> {#if itemState.showOpacity && isMapService(service)} <OpacitySlider class="gv-pl-6 gv-pr-7 gv-py-3" inputClass={cn('gv-bg-transparent', !service.visible && 'gv-text-body gv-border-body')} bind:opacity={service.opacity} variant={service.visible ? 'default' : 'secondary'} showOpacityNumber /> {/if}
<!--Description--> {#if itemState.showInfo && isMapService(service)} <div class="gv-text-sm gv-ml-11 gv-pt-2.5 gv-pb-3"> <article title={service.description}> {truncate(service.description, config.maxLengthForServiceDescription)} </article> {#if service.metadataUrl} <Button href={service.metadataUrl} variant="link" size="sm" target="_blank" onclick={onMetadataUrlClick} > <ExternalLink class="gv-size-4" /> {i18n('view-complete')} </Button> {/if} </div> {/if}
<!--Children--> {#if hasSublayers(service) && service.sublayers && open} <Sortable items={service.sublayers.list} getId={(s) => s.id.toString()} onSort={(ids) => service.sublayers.sort(ids)} reversed class={cn('gv-divide-y', isRoot ? 'gv-pl-10' : 'gv-pl-5')} > {#snippet children({ item: sublayer, bindDragTrigger })} {@const state = tocState.get(sublayer)} <TocItem bind:headerElement={() => {}, bindDragTrigger} bind:open={state.open} {legendCacheItem} {state} {i18nConfig} {config} parentVisible={service.visible && parentVisible} dataTestIdPrefix={`${dataTestId}-`} layerList={service.sublayers} {placeholder} headerClass="gv-pl-0 gv-py-1.5" /> {/snippet} </Sortable> {/if}
<!--Legend--> {#if open && legendCacheItem} {#if legendCacheItem.loading} <Loader /> {:else if legendCacheItem.error} <Error message={legendCacheItem.error.message} /> {:else if legendCacheItem.data} <LegendItemDisplay legendItems={legendCacheItem.getLegendForLayer(`${service.id}`)} class="gv-ml-10" /> {/if} {/if}
<!--Time Travel--> {#if open && isTimeTravelService(service)} <TimeTravelController config={config.timeTravelControllerConfig} i18n={i18nConfig} bind:thumbnailIndex {service} class="gv-px-5 gv-pb-3.5" /> {/if}</div>
<style> @keyframes gv-blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
.gv-animate-blink { animation: gv-blink 0.8s infinite; }</style>packages/common/src/lib/widgets/toc/components/TocLayerActions.svelte
<script lang="ts"> import { type BaseListWrapper, isGraphicService, isMapService, isQueryable, isTimeTravelService, type Queryable, } from '$lib/api/utils'; import ExportButton from './ExportButton.svelte'; import TocActionIcon from './TocActionIcon.svelte'; import ZoomToExtentButton from './ZoomToExtentButton.svelte'; import type { ApiMapService } from '$lib/api/mapservices'; import { type ApiSublayer, hasSublayers } from '$lib/api/layers'; import { getWidgetManager } from '$lib/api/managers/widget'; import { getI18n } from '$lib/api/managers/i18n'; import { getMapManager } from '$lib/api/map'; import type Draw from '$lib/widgets/draw/Draw.svelte'; import type { TocConfig, TocI18n } from '../toc.config'; import TimeTravelIcon from './TimeTravelIcon.svelte'; import Trash2 from 'lucide-svelte/icons/trash-2'; import ConfirmDialog from '$lib/components/confirm-dialog/ConfirmDialog.svelte'; import { Button } from '$lib/components/shadcn/ui/button'; import { GeoviewerError } from '$lib/api/managers/error/geoviewer-error.model'; import { getTopicManager } from '$lib/api/managers/topic'; import type ServiceDataTableWidget from '$lib/widgets/service-data-table/ServiceDataTableWidget.svelte'; import { cn } from '$lib/components/shadcn/utils'; import { getTableId } from '$lib/widgets/service-data-table/service-data-table/service-data-table.model';
interface Props { service: ApiMapService | ApiSublayer; config: TocConfig; dataTestId: string; showActions: boolean; showInfo: boolean; showOpacity: boolean; i18n: TocI18n; onPlayClicked: () => void; parentVisible: boolean; layerList: BaseListWrapper<ApiMapService | ApiSublayer>; class?: string; }
let { service, config, dataTestId, showActions = $bindable(), showInfo = $bindable(), showOpacity = $bindable(), i18n: tocI18n, layerList, parentVisible, class: className, onPlayClicked = () => {}, }: Props = $props();
const widgetManager = getWidgetManager(); const mapManager = getMapManager(); const topic = getTopicManager(); const i18n = getI18n(tocI18n); let confirmDeleteOpen = $state(false);
const graphicMapService = $derived(isGraphicService(service) ? service : null); const mapService = $derived(isMapService(service) ? service : null); const timeTravel = $derived(isTimeTravelService(service) ? service : null); const queryableService = $derived( config.dataTableWidgetId && isQueryable(service) && !hasSublayers(service) && widgetManager.hasWidgetClass('ServiceDataTable') ? service : null, ); const showTableIcon = $derived( config.dataTableWidgetId && !hasSublayers(service) && widgetManager.hasWidgetClass('ServiceDataTable') ? service : null, );
function launchDraw() { if (!config.drawWidgetId) { throw new GeoviewerError('No drawWidgetId provided'); } if (!isGraphicService(service)) { return; } widgetManager .getReference<Draw>(config.drawWidgetId) .activate() .then((widget) => widget.initFromService(service)); }
async function getServiceDataTableWidget(): Promise<ServiceDataTableWidget> { if (!config.dataTableWidgetId) { throw new GeoviewerError('No dataTableWidgetId provided'); } return await widgetManager.getReference<ServiceDataTableWidget>(config.dataTableWidgetId).activate(); }
async function openDataTable(queryable: (typeof service & Queryable) | null) { if (!queryable) return; const tabName = queryable.label ?? ''; const tabId = getTableId(queryable); const dataTableWidget = await getServiceDataTableWidget(); dataTableWidget.openDataTable({ tabName, service: queryable, tabId }); }
function toggleDescription() { showInfo = !showInfo; showOpacity = false;
if (showInfo) { topic.publish({ type: 'Toc-Layer-description-open', layer: mapService!, }); } }
function onShowOpacity() { showOpacity = !showOpacity; showInfo = false; }
function confirmDelete() { confirmDeleteOpen = true; }
function deleteLayer() { deleteAssociatedServiceDataTableResults(); layerList.remove(service); topic.publish({ type: 'Toc-Layer-remove', layer: service, }); }
async function deleteAssociatedServiceDataTableResults() { const serviceDataTableWidget = await getServiceDataTableWidget(); serviceDataTableWidget.closeAllPanelsForServiceId(getTableId(service)); }
function toggleVisibility(toggle: boolean) { service.visible = toggle; topic.publish({ type: 'Toc-Layer-visibility-toggle', layer: service, toggle, }); }</script>
<div class={cn('gv-flex gv-flex-row gv-items-center', className)}> {#if showTableIcon} <TocActionIcon data-test-id={`${dataTestId}-Toc-Search`} title={i18n('common.search')} disabled={!queryableService} tooltip={queryableService ? '' : i18n('query-disabled')} onclick={() => openDataTable(queryableService)} icon={{ lucide: 'TableProperties' }} visibleOnMap={service.visible && parentVisible} class="gv-hidden group-hover:gv-inline-block" /> {/if}
{#if isTimeTravelService(service)} <TimeTravelIcon {onPlayClicked} {service} config={config.timeTravelControllerConfig} /> {/if}
<!--Graphic service edition--> {#if graphicMapService && !graphicMapService.editing && widgetManager.hasWidgetClass('Draw')} <TocActionIcon data-test-id={`${dataTestId}-Toc-Draw`} title={i18n('common.edit')} onclick={() => launchDraw()} icon={{ lucide: 'Pencil' }} visibleOnMap={service.visible && parentVisible} /> {/if}
<!--Layer visible--> {#if service.visible} <TocActionIcon data-test-id={`${dataTestId}-Toc-hide`} title={i18n('common.hide')} onclick={() => toggleVisibility(false)} icon={{ lucide: 'Eye' }} visibleOnMap={parentVisible} /> {:else} <TocActionIcon data-test-id={`${dataTestId}-Toc-show`} title={i18n('common.show')} onclick={() => toggleVisibility(true)} icon={{ lucide: 'EyeOff' }} visibleOnMap={false} /> {/if}
{#if showActions} {#if graphicMapService} <ExportButton data-test-id={`${dataTestId}-Toc-export`} title={i18n('common.export')} service={graphicMapService} {parentVisible} /> {/if} {#if mapService} <ZoomToExtentButton data-test-id={`${dataTestId}-Toc-extent`} title={i18n('zoom-to-extent')} service={mapService} noExtentMessage={i18n('no-extent')} {mapManager} {parentVisible} /> {#if !graphicMapService && !timeTravel} <TocActionIcon title={i18n('common.description')} data-test-id={`${dataTestId}-Toc-description`} active={showInfo} onclick={toggleDescription} icon={{ lucide: 'Info' }} visibleOnMap={service.visible && parentVisible} /> {/if} <TocActionIcon title={i18n('opacity')} active={showOpacity} data-test-id={`${dataTestId}-Toc-opacity`} onclick={onShowOpacity} icon={{ lucide: 'Contrast' }} visibleOnMap={service.visible && parentVisible} /> {/if} {#if mapService && mapService.removable} <TocActionIcon data-test-id={`${dataTestId}-Toc-remove`} title={i18n('remove')} onclick={() => confirmDelete()} disabled={graphicMapService?.editing} icon={{ lucide: 'Trash2' }} visibleOnMap={service.visible && parentVisible} /> {/if} {/if}
<!--Toggle actions--> {#if graphicMapService || mapService} <TocActionIcon title={i18n('other-actions')} data-test-id={`${dataTestId}-Toc-actions-menu`} onclick={() => (showActions = !showActions)} icon={{ lucide: 'Ellipsis' }} visibleOnMap={service.visible && parentVisible} /> {/if}</div>
<ConfirmDialog message={i18n('confirm-delete-message')} bind:open={confirmDeleteOpen}> {#snippet footer()} <Button onclick={() => (confirmDeleteOpen = false)} variant="outline" data-test-id="TocItem-ConfirmDelete-Cancel" > {i18n('common.cancel')} </Button> <Button onclick={deleteLayer} data-test-id="TocItem-ConfirmDelete-Confirm" variant="destructive"> <Trash2 class="gv-size-4" /> {i18n('common.confirm')} </Button> {/snippet}</ConfirmDialog>packages/common/src/lib/widgets/toc/components/TocLayerIcon.svelte
<script lang="ts"> import { cn } from '$lib/components/shadcn/utils'; import { isMapService, isTimeTravelService } from '$lib/api/utils'; import History from 'lucide-svelte/icons/history'; import Map from 'lucide-svelte/icons/map'; import { Icon } from '$lib/components/icon'; import ChevronRight from 'lucide-svelte/icons/chevron-right'; import ChevronsUpDown from 'lucide-svelte/icons/chevrons-up-down'; import type { ApiMapService } from '$lib/api/mapservices'; import type { ApiSublayer } from '$lib/api/layers'; import { Tangent } from 'lucide-svelte';
interface Props { isRoot: boolean; service: ApiMapService | ApiSublayer; open: boolean; parentVisible: boolean; class?: string; }
let { isRoot, service, open, parentVisible, class: className }: Props = $props(); const iconClass = $derived( cn('gv-size-4 gv-text-body', service.visible && parentVisible && 'gv-text-primary-500', className), ); const isFeatureService = $derived.by( () => isMapService(service) && ['ARCGIS_FEATURE_SERVICE', 'ARCGIS_FEATURE_LAYER'].indexOf(service.type) > -1, );</script>
{#if isRoot} <div class="gv-block group-hover:gv-hidden"> {#if isMapService(service)} {#if service.type === 'GRAPHICS'} <Icon class={iconClass} icon={{ geoviewer: 'graphic-layer' }} /> {:else if isFeatureService} <Tangent class={iconClass} /> {:else if isTimeTravelService(service)} <History class={iconClass} /> {:else} <Map class={iconClass} /> {/if} {:else} <Map class={iconClass} /> {/if} </div>{:else} <!--Expand chevron--> <ChevronRight class={cn(iconClass, 'gv-duration-150 group-hover:gv-hidden', open && 'gv-rotate-90')} />{/if}<ChevronsUpDown class={cn(iconClass, 'gv-hidden group-hover:gv-inline-block')} />packages/common/src/lib/widgets/toc/components/ZoomToExtentButton.svelte
<script lang="ts"> import type { MapManager } from '$lib/api/map'; import type { ApiMapService } from '$lib/api/mapservices'; import TocActionIcon from './TocActionIcon.svelte'; import type { HTMLButtonAttributes } from 'svelte/elements'; import { showToast } from '$lib/components/toast/toast.utils'; import { isTimeTravelService } from '$lib/api/utils';
interface Props extends HTMLButtonAttributes { service: ApiMapService; mapManager: MapManager; parentVisible: boolean; noExtentMessage: string; }
let { service, mapManager, parentVisible, noExtentMessage, ...props }: Props = $props(); function zoomToFullExtent() { let fullExtent = service.extent; if (isTimeTravelService(service)) { fullExtent = service.services[0].extent; } if (fullExtent) { mapManager.tools.zoom.zoomToExtent(fullExtent); } else { showToast({ level: 'info', message: noExtentMessage }); } }</script>
<TocActionIcon {...props} onclick={zoomToFullExtent} icon={{ lucide: 'Fullscreen' }} visibleOnMap={service.visible && parentVisible}/>packages/common/src/lib/widgets/toc/toc.i18n.ts
import type { I18nRegistry } from '$lib/api/managers/i18n';import { basemapChooserI18n } from '$lib/widgets/basemap-chooser/basemap-chooser.i18n';
export const tocI18n = { title: { fr: 'Données', nl: 'NL - Données', }, 'add-data': { fr: 'Ajouter des données', nl: 'NL - Ajouter des données', }, 'empty-selection': { fr: 'Supprimer toutes les données', nl: 'NL - Supprimer toutes les données', }, 'empty-selection-confirmation': { fr: 'Voulez-vous supprimer le contenu actuel de votre carte ?', nl: 'NL - Voulez-vous supprimer le contenu actuel de votre carte ?', }, 'layer-label-empty': { fr: 'Couche sans titre', nl: 'NL - Couche sans titre', }, 'zoom-to-extent': { fr: "Zoomer sur l'emprise", nl: "NL - Zoomer sur l'emprise", }, 'no-extent': { fr: "L'emprise n'a pas pu être calculée", nl: "NL - L'emprise n'a pas pu être calculée", }, 'other-actions': { fr: "Autres d'actions", nl: "NL - Autres d'actions", }, 'query-disabled': { fr: 'Fonctionnalité non disponible pour cette donnée', nl: 'NL - Fonctionnalité non disponible pour cette donnée', }, 'confirm-delete-message': { fr: 'Voulez-vous supprimer la donnée de la carte ?', nl: 'NL - Voulez-vous supprimer la donnée de la carte ?', }, remove: { fr: 'Retirer de la carte', nl: 'NL - Retirer de la carte', }, opacity: { fr: "Modifier l'opacité", nl: "NL - Modifier l'opacité", }, editing: { fr: 'Edition en cours', nl: 'NL - Edition en cours', }, 'view-complete': { fr: 'Ouvrir la fiche descriptive', nl: 'NL - Ouvrir la fiche descriptive', }, 'view-complete-label': { fr: 'Ouvrir la fiche descriptive de {{label}}', nl: 'NL - Ouvrir la fiche descriptive de {{label}}', }, 'visible-above': { fr: 'Zoomez jusque 1:{{maxScale}}', nl: 'NL - Zoomez jusque 1:{{maxScale}}', }, 'visible-under': { fr: 'Zoomez à partir de 1:{{minScale}}', nl: 'NL - Zoomez à partir de 1:{{minScale}}', }, 'zoom-in-or-out': { fr: 'Zoomez ou dézoomez pour voir les données (1:{{minScale}} - 1:{{maxScale}})', nl: 'NL - Zoomez ou dézoomez pour voir les données (1:{{minScale}} - 1:{{maxScale}})', }, 'basemap-title': { fr: 'Fond de plan', nl: 'NL - Fond de plan', }, 'basemap-view-complete-data': { fr: 'Ouvrir la fiche descriptive', nl: 'NL - Ouvrir la fiche descriptive', }, ...basemapChooserI18n,} satisfies I18nRegistry;packages/common/src/lib/widgets/toc/toc.state.svelte.ts
import type { ApiSublayer } from '$lib/api/layers';import type { WidgetStateProps } from '$lib/api/managers/widget/widget-declaration';import type { ApiMapService } from '$lib/api/mapservices';import { getContext, setContext } from 'svelte';import { SvelteMap } from 'svelte/reactivity';
export class TocState { readonly states = new Map<ApiMapService | ApiSublayer, TocItemState>(); readonly highlighted = $state(new SvelteMap<string, boolean>()); basemapSelectorOpen = $state(false);
constructor(private readonly props: WidgetStateProps) {}
public get(service: ApiMapService | ApiSublayer) { if (this.states.has(service)) { return this.states.get(service)!; } const state = new TocItemState(service); this.states.set(service, state); return state; }
expandService(serviceId: string) { const entry = [...this.states.entries()].find(([service]) => service.id === serviceId); if (entry) { const [_, state] = entry; state.open = true; } }
highlightService(serviceId: string, durationInMs: number = 3000): void { this.highlighted.set(serviceId, true); setTimeout(() => { this.highlighted.delete(serviceId); }, durationInMs); }
openBasemapSelector() { this.basemapSelectorOpen = true; }
closeBasemapSelector() { this.basemapSelectorOpen = false; }}
export class TocItemState { _showActions = $state(false); showInfo = $state(false); showOpacity = $state(false);
get open() { return this.service.toc.open; }
set open(value: boolean) { this.service.toc.open = value; }
constructor(public readonly service: ApiMapService | ApiSublayer) {}
get showActions() { return this._showActions; }
set showActions(v: boolean) { if (!v) { this.showInfo = false; this.showOpacity = false; } this._showActions = v; }}
export function setState(state: TocState) { setContext('state-toc', state);}
export function getState() { const state = getContext<TocState>('state-toc'); if (!state) { throw new Error('No toc state set in context'); } return state;}packages/common/src/lib/widgets/toc/Toc.svelte
<script lang="ts"> import { getI18n } from '$lib/api/managers/i18n'; import { getWidgetManager } from '$lib/api/managers/widget'; import { getMapManager } from '$lib/api/map'; import ConfirmDialog from '$lib/components/confirm-dialog/ConfirmDialog.svelte'; import { Icon } from '$lib/components/icon'; import { Button } from '$lib/components/shadcn/ui/button'; import { Sortable } from '$lib/components/sortable'; import Trash2 from 'lucide-svelte/icons/trash-2'; import TocItem from './components/TocItem.svelte'; import TocBasemapSelector from './components/TocBasemapSelector.svelte'; import type { TocProps } from './toc.declaration'; import { setState } from './toc.state.svelte'; import { GeoviewerError } from '$lib/api/managers/error'; import type ServiceDataTableWidget from '$lib/widgets/service-data-table/ServiceDataTableWidget.svelte';
const props: TocProps = $props(); const { fullConfig, state: tocState } = props; const { config } = fullConfig;
setState(tocState); const i18n = getI18n(fullConfig.i18n); const mapManager = getMapManager(); const widgetManager = getWidgetManager();
const displayEmptySelectionButton = $derived.by( () => layerList.list.filter((service) => service.toc.visible).length > 0, );
let layerList = mapManager.layerList; let confirmOpened = $state<boolean>(false); let basemapSelectorOpen = $state<boolean>(false);
export function highlightService(serviceId: string, durationInMs: number = 3000): void { tocState.highlightService(serviceId, durationInMs); }
export function expandService(serviceId: string): void { tocState.expandService(serviceId); }
export function openBasemapSelector() { basemapSelectorOpen = true; }
export function closeBasemapSelector() { basemapSelectorOpen = false; }
function openAddData() { widgetManager.getReference(config.addDataWidgetId).activate(); }
function emptySelection() { confirmOpened = false; closeAllServiceDataTables(); layerList.removeAll(); }
async function closeAllServiceDataTables() { if (!config.dataTableWidgetId) { throw new GeoviewerError('No dataTableWidgetId provided'); } const dataTableWidget = await widgetManager .getReference<ServiceDataTableWidget>(config.dataTableWidgetId) .activate(); dataTableWidget.closeAllPanels(); }</script>
<div class="gv-h-full gv-flex gv-flex-col gv-min-h-0"> {#if !basemapSelectorOpen} {#if config.showAddDataButton} <div class="gv-px-5 gv-py-2.5 gv-border-b"> <Button onclick={openAddData} data-test-id="Toc-AddDataButton" size="sm"> <Icon class="gv-size-4 gv-align-baseline" icon={{ geoviewer: 'add-layer' }} /> {i18n('add-data')} </Button> </div> {/if}
<div class="gv-flex gv-flex-col gv-flex-1 gv-overflow-y-auto"> <div class="gv-overflow-y-auto"> <Sortable items={layerList.list} getId={(s) => s.id} onSort={(ids) => layerList.sort(ids)} reversed> {#snippet children({ item: service, bindDragTrigger })} {#if service.toc.visible} {@const state = tocState.get(service)} <TocItem bind:headerElement={() => {}, bindDragTrigger} isRoot {state} bind:open={state.open} highlight={tocState.highlighted.has(service.id)} {config} i18nConfig={fullConfig.i18n} {layerList} placeholder={i18n('layer-label-empty')} class="gv-w-full gv-border-b" /> {/if} {/snippet} </Sortable> </div>
{#if config.showClearAllButton && displayEmptySelectionButton} <div class="gv-flex gv-justify-center gv-p-3 gv-bottom-0 gv-w-full gv-bg-background"> <Button variant="link" class="gv-text-destructive hover:gv-no-underline" onclick={() => (confirmOpened = true)} data-test-id="Toc-EmptySelectionButton" > <Trash2 class="gv-size-4" /> {i18n('empty-selection')} </Button> </div> {/if} </div> {/if}
{#if config.showBasemapSelector} <TocBasemapSelector bind:open={basemapSelectorOpen} i18nRegistry={fullConfig.i18n} /> {/if}</div>
<ConfirmDialog message={i18n('empty-selection-confirmation')} bind:open={confirmOpened}> {#snippet footer()} <Button onclick={() => (confirmOpened = false)} variant="outline" data-test-id="Toc-EmptySelectionButton-Cancel" > {i18n('common.cancel')} </Button> <Button onclick={emptySelection} data-test-id="Toc-EmptySelectionButton-Confirm" variant="destructive"> <Trash2 class="gv-size-4" /> {i18n('common.confirm')} </Button> {/snippet}</ConfirmDialog>packages/common/src/lib/widgets/toc/toc.topic.ts
import type { ApiMapService } from '$lib/api/mapservices';import type { ApiSublayer } from '$lib/api/layers';
export type TocTopicEvent = | { type: 'Toc-Layer-visibility-toggle'; toggle: boolean; layer: ApiMapService | ApiSublayer; } | { type: 'Toc-Layer-remove'; layer: ApiMapService | ApiSublayer; } | { type: 'Toc-Layer-description-open'; layer: ApiMapService; } | { type: 'Toc-Layer-metadata-open'; layer: ApiMapService; };packages/common/src/lib/widgets/toc/toc.utils.ts
import type Toc from '$lib/widgets/toc/Toc.svelte';import type WidgetManager from '$lib/api/managers/widget/widget.manager.svelte';import type { MapManager } from '$lib/api/map';import type { TocState } from '$lib/widgets/toc/toc.state.svelte';
export function highlightServiceInToc( mapServiceId: string, widgetManager: WidgetManager, mapManager: MapManager,): void { const service = mapManager.layerList.list.find((layer) => layer.id === mapServiceId); if (service) { service.onLoad(() => { const [tocReference] = widgetManager.getReferencesByWidgetClass<Toc, TocState>('Toc'); if (tocReference) { tocReference.state.highlightService(mapServiceId); tocReference .activate({ openContainer: true }) .catch((err) => console.error('Failed on activate toc', err)); } }); }}
export function openServiceInToc(mapServiceId: string, widgetManager: WidgetManager): void { const [tocReference] = widgetManager.getReferencesByWidgetClass<Toc, TocState>('Toc'); if (tocReference) { tocReference.activate().catch((err) => console.error('Failed on activate toc', err)); tocReference.state.expandService(mapServiceId); }}