Merge pull request #10845 from tchak/update-maplibre
chore(maplibre): update
This commit is contained in:
commit
0321226130
21 changed files with 5936 additions and 5826 deletions
|
@ -1,6 +1,4 @@
|
|||
@import "colors";
|
||||
@import "constants";
|
||||
|
||||
|
||||
.areas {
|
||||
margin-bottom: 10px;
|
||||
|
@ -10,56 +8,49 @@
|
|||
}
|
||||
}
|
||||
|
||||
.map-style-control {
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
left: 10px;
|
||||
.ds-ctrl button {
|
||||
color: $dark-grey;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
> div {
|
||||
position: absolute;
|
||||
bottom: 5px;
|
||||
left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.map-style-panel {
|
||||
z-index: 1;
|
||||
padding: $default-spacer;
|
||||
margin-bottom: $default-spacer;
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: $default-spacer;
|
||||
padding-bottom: 0;
|
||||
margin-bottom: -$default-spacer;
|
||||
|
||||
label {
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
&.on,
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.cadastres-selection-control {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
top: 135px;
|
||||
left: 10px;
|
||||
.react-aria-popover {
|
||||
&[data-placement='top'] {
|
||||
--origin: translateY(8px);
|
||||
}
|
||||
|
||||
button {
|
||||
&.on,
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
&[data-placement='bottom'] {
|
||||
--origin: translateY(-8px);
|
||||
}
|
||||
|
||||
&[data-placement='right'] {
|
||||
--origin: translateX(-8px);
|
||||
}
|
||||
|
||||
&[data-placement='left'] {
|
||||
--origin: translateX(8px);
|
||||
}
|
||||
|
||||
&[data-entering] {
|
||||
animation: popover-slide 200ms;
|
||||
}
|
||||
|
||||
&[data-exiting] {
|
||||
animation: popover-slide 200ms reverse ease-in;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes popover-slide {
|
||||
from {
|
||||
transform: var(--origin);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { useCallback, useRef } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import type { Feature, FeatureCollection } from 'geojson';
|
||||
import { CursorClickIcon } from '@heroicons/react/outline';
|
||||
|
||||
import { useMapLibre } from '../../shared/maplibre/MapLibre';
|
||||
import { useMapLibre, ReactControl } from '../../shared/maplibre/MapLibre';
|
||||
import {
|
||||
useEvent,
|
||||
useMapEvent,
|
||||
|
@ -18,15 +20,31 @@ export function CadastreLayer({
|
|||
featureCollection,
|
||||
createFeatures,
|
||||
deleteFeatures,
|
||||
toggle,
|
||||
enabled
|
||||
}: {
|
||||
featureCollection: FeatureCollection;
|
||||
createFeatures: CreateFeatures;
|
||||
deleteFeatures: DeleteFeatures;
|
||||
toggle: () => void;
|
||||
enabled: boolean;
|
||||
}) {
|
||||
const map = useMapLibre();
|
||||
const selectedCadastresRef = useRef(new Set<string>());
|
||||
const [controlElement, setControlElement] = useState<HTMLElement | null>(
|
||||
null
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const control = new ReactControl();
|
||||
map.addControl(control, 'top-left');
|
||||
setControlElement(control.container);
|
||||
|
||||
return () => {
|
||||
map.removeControl(control);
|
||||
setControlElement(null);
|
||||
};
|
||||
}, [map, enabled]);
|
||||
|
||||
const highlightFeature = useCallback(
|
||||
(cid: string, highlight: boolean) => {
|
||||
|
@ -95,7 +113,35 @@ export function CadastreLayer({
|
|||
|
||||
useEvent('map:internal:cadastre:highlight', onHighlight);
|
||||
|
||||
return null;
|
||||
return (
|
||||
<>
|
||||
{controlElement != null
|
||||
? createPortal(
|
||||
<CadastreSwitch enabled={enabled} toggle={toggle} />,
|
||||
controlElement
|
||||
)
|
||||
: null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function CadastreSwitch({
|
||||
enabled,
|
||||
toggle
|
||||
}: {
|
||||
enabled: boolean;
|
||||
toggle: () => void;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggle}
|
||||
title="Sélectionner les parcelles cadastrales"
|
||||
className={enabled ? 'on' : 'off'}
|
||||
>
|
||||
<CursorClickIcon className="icon-size" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function useCadastres(
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { useCallback, useRef, useEffect } from 'react';
|
||||
import type { LngLatBoundsLike, LngLatLike } from 'maplibre-gl';
|
||||
import type { LngLatBoundsLike, LngLatLike, IControl } from 'maplibre-gl';
|
||||
import DrawControl from '@mapbox/mapbox-gl-draw';
|
||||
import type { FeatureCollection, Feature, Point } from 'geojson';
|
||||
|
||||
|
@ -52,18 +52,14 @@ export function DrawLayer({
|
|||
});
|
||||
// We use mapbox-draw plugin with maplibre. They are compatible but types are not.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
map.addControl(draw as any, 'top-left');
|
||||
const control = draw as any as IControl;
|
||||
map.addControl(control, 'top-left');
|
||||
draw.set(
|
||||
filterFeatureCollection(featureCollection, SOURCE_SELECTION_UTILISATEUR)
|
||||
);
|
||||
drawRef.current = draw;
|
||||
|
||||
for (const [selector, translation] of translations) {
|
||||
const element = document.querySelector(selector);
|
||||
if (element) {
|
||||
element.setAttribute('title', translation);
|
||||
}
|
||||
}
|
||||
patchDrawControl();
|
||||
}
|
||||
|
||||
return () => {
|
||||
|
@ -228,3 +224,15 @@ const translations = [
|
|||
['.mapbox-gl-draw_point', 'Ajouter un point'],
|
||||
['.mapbox-gl-draw_trash', 'Supprimer']
|
||||
];
|
||||
|
||||
function patchDrawControl() {
|
||||
document.querySelectorAll('.mapboxgl-ctrl').forEach((control) => {
|
||||
control.classList.add('maplibregl-ctrl', 'maplibregl-ctrl-group');
|
||||
|
||||
for (const [selector, translation] of translations) {
|
||||
for (const button of control.querySelectorAll(selector)) {
|
||||
button.setAttribute('title', translation);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import { useState } from 'react';
|
||||
import { CursorClickIcon } from '@heroicons/react/outline';
|
||||
import 'maplibre-gl/dist/maplibre-gl.css';
|
||||
import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css';
|
||||
import type { FeatureCollection } from 'geojson';
|
||||
|
||||
|
@ -51,25 +49,12 @@ export default function MapEditor({
|
|||
enabled={!cadastreEnabled}
|
||||
/>
|
||||
{options.layers.includes('cadastres') ? (
|
||||
<>
|
||||
<CadastreLayer
|
||||
featureCollection={featureCollection}
|
||||
{...actions}
|
||||
enabled={cadastreEnabled}
|
||||
/>
|
||||
<div className="cadastres-selection-control mapboxgl-ctrl-group">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setCadastreEnabled((cadastreEnabled) => !cadastreEnabled)
|
||||
}
|
||||
title="Sélectionner les parcelles cadastrales"
|
||||
className={cadastreEnabled ? 'on' : ''}
|
||||
>
|
||||
<CursorClickIcon className="icon-size" />
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
<CadastreLayer
|
||||
featureCollection={featureCollection}
|
||||
{...actions}
|
||||
toggle={() => setCadastreEnabled((enabled) => !enabled)}
|
||||
enabled={cadastreEnabled}
|
||||
/>
|
||||
) : null}
|
||||
</MapLibre>
|
||||
<PointInput featureCollection={featureCollection} />
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import 'maplibre-gl/dist/maplibre-gl.css';
|
||||
import type { FeatureCollection } from 'geojson';
|
||||
|
||||
import { MapLibre } from '../shared/maplibre/MapLibre';
|
||||
|
|
|
@ -8,13 +8,15 @@ import {
|
|||
createContext,
|
||||
useCallback
|
||||
} from 'react';
|
||||
import maplibre, { Map, NavigationControl } from 'maplibre-gl';
|
||||
import type { Style } from 'maplibre-gl';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Map, NavigationControl } from 'maplibre-gl';
|
||||
import type { StyleSpecification, IControl } from 'maplibre-gl';
|
||||
import 'maplibre-gl/dist/maplibre-gl.css';
|
||||
|
||||
import invariant from 'tiny-invariant';
|
||||
|
||||
import { useStyle, useElementVisible } from './hooks';
|
||||
import { StyleControl } from './StyleControl';
|
||||
import { StyleSwitch } from './StyleControl';
|
||||
|
||||
const Context = createContext<{ map?: Map | null }>({});
|
||||
|
||||
|
@ -30,16 +32,15 @@ export function useMapLibre() {
|
|||
}
|
||||
|
||||
export function MapLibre({ children, layers }: MapLibreProps) {
|
||||
const isSupported = useMemo(
|
||||
() => maplibre.supported({ failIfMajorPerformanceCaveat: true }) && !isIE(),
|
||||
[]
|
||||
);
|
||||
const isSupported = useMemo(() => isWebglSupported(), []);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const visible = useElementVisible(containerRef);
|
||||
const [map, setMap] = useState<Map | null>();
|
||||
const [styleControlElement, setStyleControlElement] =
|
||||
useState<HTMLElement | null>(null);
|
||||
|
||||
const onStyleChange = useCallback(
|
||||
(style: Style) => {
|
||||
(style: StyleSpecification) => {
|
||||
if (map) {
|
||||
map.setStyle(style);
|
||||
}
|
||||
|
@ -56,8 +57,11 @@ export function MapLibre({ children, layers }: MapLibreProps) {
|
|||
style
|
||||
});
|
||||
map.addControl(new NavigationControl({}), 'top-right');
|
||||
const styleControl = new ReactControl();
|
||||
map.addControl(styleControl, 'bottom-left');
|
||||
map.on('load', () => {
|
||||
setMap(map);
|
||||
setStyleControlElement(styleControl.container);
|
||||
});
|
||||
}
|
||||
}, [map, style, visible, isSupported]);
|
||||
|
@ -91,16 +95,54 @@ export function MapLibre({ children, layers }: MapLibreProps) {
|
|||
return (
|
||||
<Context.Provider value={{ map }}>
|
||||
<div ref={containerRef} style={{ height: '500px' }}>
|
||||
<StyleControl styleId={style.id} {...mapStyleProps} />
|
||||
{styleControlElement != null
|
||||
? createPortal(
|
||||
<StyleSwitch styleId={style.id} {...mapStyleProps} />,
|
||||
styleControlElement
|
||||
)
|
||||
: null}
|
||||
{map ? children : null}
|
||||
</div>
|
||||
</Context.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function isIE() {
|
||||
const ua = window.navigator.userAgent;
|
||||
const msie = ua.indexOf('MSIE ');
|
||||
const trident = ua.indexOf('Trident/');
|
||||
return msie > 0 || trident > 0;
|
||||
function isWebglSupported() {
|
||||
if (window.WebGLRenderingContext) {
|
||||
const canvas = document.createElement('canvas');
|
||||
try {
|
||||
// Note that { failIfMajorPerformanceCaveat: true } can be passed as a second argument
|
||||
// to canvas.getContext(), causing the check to fail if hardware rendering is not available. See
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/getContext
|
||||
// for more details.
|
||||
const context = canvas.getContext('webgl2') || canvas.getContext('webgl');
|
||||
if (context && typeof context.getParameter == 'function') {
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
// WebGL is supported, but disabled
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// WebGL not supported
|
||||
return false;
|
||||
}
|
||||
|
||||
export class ReactControl implements IControl {
|
||||
#container: HTMLElement | null = null;
|
||||
|
||||
get container(): HTMLElement | null {
|
||||
return this.#container;
|
||||
}
|
||||
|
||||
onAdd(): HTMLElement {
|
||||
this.#container = document.createElement('div');
|
||||
this.#container.className = 'maplibregl-ctrl maplibregl-ctrl-group ds-ctrl';
|
||||
return this.#container;
|
||||
}
|
||||
|
||||
onRemove(): void {
|
||||
this.#container?.remove();
|
||||
this.#container = null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { useState, useId } from 'react';
|
||||
import { Popover, RadioGroup } from '@headlessui/react';
|
||||
import { usePopper } from 'react-popper';
|
||||
import { useId, useRef, useEffect } from 'react';
|
||||
import { Button, Dialog, DialogTrigger, Popover } from 'react-aria-components';
|
||||
import { MapIcon } from '@heroicons/react/outline';
|
||||
import { Slider } from '@reach/slider';
|
||||
import '@reach/slider/styles.css';
|
||||
|
@ -13,7 +12,7 @@ const STYLES = {
|
|||
ign: 'Carte IGN'
|
||||
};
|
||||
|
||||
export function StyleControl({
|
||||
export function StyleSwitch({
|
||||
styleId,
|
||||
layers,
|
||||
setStyle,
|
||||
|
@ -26,107 +25,97 @@ export function StyleControl({
|
|||
setLayerEnabled: (layer: string, enabled: boolean) => void;
|
||||
setLayerOpacity: (layer: string, opacity: number) => void;
|
||||
}) {
|
||||
const [buttonElement, setButtonElement] =
|
||||
useState<HTMLButtonElement | null>();
|
||||
const [panelElement, setPanelElement] = useState<HTMLDivElement | null>();
|
||||
const { styles, attributes } = usePopper(buttonElement, panelElement, {
|
||||
placement: 'bottom-end'
|
||||
});
|
||||
const configurableLayers = Object.entries(layers).filter(
|
||||
([, { configurable }]) => configurable
|
||||
);
|
||||
const mapId = useId();
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (buttonRef.current) {
|
||||
buttonRef.current.title = 'Sélectionner les couches cartographiques';
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="form map-style-control mapboxgl-ctrl-group"
|
||||
style={{ zIndex: 10 }}
|
||||
>
|
||||
<Popover>
|
||||
<Popover.Button
|
||||
ref={setButtonElement}
|
||||
className="map-style-button"
|
||||
title="Sélectionner les couches cartographiques"
|
||||
>
|
||||
<MapIcon className="icon-size" />
|
||||
</Popover.Button>
|
||||
<Popover.Panel
|
||||
className="flex map-style-panel mapboxgl-ctrl-group"
|
||||
ref={setPanelElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
<RadioGroup
|
||||
value={styleId}
|
||||
onChange={setStyle}
|
||||
className="styles-list"
|
||||
as="ul"
|
||||
<DialogTrigger>
|
||||
<Button ref={buttonRef}>
|
||||
<MapIcon className="icon-size" />
|
||||
</Button>
|
||||
<Popover className="react-aria-popover">
|
||||
<Dialog className="fr-modal__body">
|
||||
<form
|
||||
className="fr-modal__content flex m-2"
|
||||
onSubmit={(event) => event.preventDefault()}
|
||||
>
|
||||
{Object.entries(STYLES).map(([style, title]) => (
|
||||
<RadioGroup.Option
|
||||
key={style}
|
||||
value={style}
|
||||
as="li"
|
||||
className="flex"
|
||||
>
|
||||
{({ checked }) => (
|
||||
<>
|
||||
<div className="fr-fieldset">
|
||||
{Object.entries(STYLES).map(([style, title]) => (
|
||||
<div className="fr-fieldset__element" key={style}>
|
||||
<div className="fr-radio-group">
|
||||
<input
|
||||
id={`${mapId}-${style}`}
|
||||
value={style}
|
||||
type="radio"
|
||||
key={`${style}-${checked}`}
|
||||
defaultChecked={checked}
|
||||
name="map-style"
|
||||
className="m-0 p-0 mr-1"
|
||||
/>
|
||||
<RadioGroup.Label>
|
||||
{title.replace(/\s/g, ' ')}
|
||||
</RadioGroup.Label>
|
||||
</>
|
||||
)}
|
||||
</RadioGroup.Option>
|
||||
))}
|
||||
</RadioGroup>
|
||||
{configurableLayers.length ? (
|
||||
<ul className="layers-list">
|
||||
{configurableLayers.map(([layer, { enabled, opacity, name }]) => (
|
||||
<li key={layer}>
|
||||
<div className="flex mb-1">
|
||||
<input
|
||||
id={`${mapId}-${layer}`}
|
||||
className="m-0 p-0 mr-1"
|
||||
type="checkbox"
|
||||
checked={enabled}
|
||||
defaultValue={style}
|
||||
checked={styleId == style}
|
||||
onChange={(event) => {
|
||||
setLayerEnabled(layer, event.target.checked);
|
||||
setStyle(event.target.value);
|
||||
}}
|
||||
/>
|
||||
<label className="m-0" htmlFor={`${mapId}-${layer}`}>
|
||||
{name}
|
||||
<label htmlFor={`${mapId}-${style}`} className="fr-label">
|
||||
{title.replace(/\s/g, ' ')}
|
||||
</label>
|
||||
</div>
|
||||
<Slider
|
||||
min={10}
|
||||
max={100}
|
||||
step={5}
|
||||
value={opacity}
|
||||
onChange={(value) => {
|
||||
setLayerOpacity(layer, value);
|
||||
}}
|
||||
className="mb-1"
|
||||
title={`Réglage de l’opacité de la couche «${NBS}${name}${NBS}»`}
|
||||
getAriaLabel={() =>
|
||||
`Réglage de l’opacité de la couche «${NBS}${name}${NBS}»`
|
||||
}
|
||||
getAriaValueText={(value) =>
|
||||
`L’opacité de la couche «${NBS}${name}${NBS}» est à ${value}${NBS}%`
|
||||
}
|
||||
/>
|
||||
</li>
|
||||
</div>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</Popover.Panel>
|
||||
</div>
|
||||
{configurableLayers.length ? (
|
||||
<div className="fr-fieldset__element">
|
||||
{configurableLayers.map(
|
||||
([layer, { enabled, opacity, name }]) => (
|
||||
<div key={layer} className="fr-fieldset__element">
|
||||
<div className="fr-checkbox-group">
|
||||
<input
|
||||
id={`${mapId}-${layer}`}
|
||||
type="checkbox"
|
||||
checked={enabled}
|
||||
onChange={(event) => {
|
||||
setLayerEnabled(layer, event.target.checked);
|
||||
}}
|
||||
/>
|
||||
<label
|
||||
className="fr-label"
|
||||
htmlFor={`${mapId}-${layer}`}
|
||||
>
|
||||
{name}
|
||||
</label>
|
||||
</div>
|
||||
<Slider
|
||||
min={10}
|
||||
max={100}
|
||||
step={5}
|
||||
value={opacity}
|
||||
onChange={(value) => {
|
||||
setLayerOpacity(layer, value);
|
||||
}}
|
||||
className="fr-range fr-range--sm mt-1"
|
||||
title={`Réglage de l’opacité de la couche «${NBS}${name}${NBS}»`}
|
||||
getAriaLabel={() =>
|
||||
`Réglage de l’opacité de la couche «${NBS}${name}${NBS}»`
|
||||
}
|
||||
getAriaValueText={(value) =>
|
||||
`L’opacité de la couche «${NBS}${name}${NBS}» est à ${value}${NBS}%`
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</form>
|
||||
</Dialog>
|
||||
</Popover>
|
||||
</div>
|
||||
</DialogTrigger>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import type {
|
|||
LngLatBoundsLike,
|
||||
LngLat,
|
||||
MapLayerEventType,
|
||||
Style,
|
||||
StyleSpecification,
|
||||
LngLatLike
|
||||
} from 'maplibre-gl';
|
||||
import type { Feature, Geometry } from 'geojson';
|
||||
|
@ -104,7 +104,7 @@ function optionalLayersMap(optionalLayers: string[]): LayersMap {
|
|||
|
||||
export function useStyle(
|
||||
optionalLayers: string[],
|
||||
onStyleChange: (style: Style) => void
|
||||
onStyleChange: (style: StyleSpecification) => void
|
||||
) {
|
||||
const [styleId, setStyle] = useState('ortho');
|
||||
const [layers, setLayers] = useState(() => optionalLayersMap(optionalLayers));
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
import type { AnyLayer, Style, RasterLayer, RasterSource } from 'maplibre-gl';
|
||||
import type {
|
||||
LayerSpecification,
|
||||
RasterLayerSpecification,
|
||||
RasterSourceSpecification,
|
||||
StyleSpecification
|
||||
} from 'maplibre-gl';
|
||||
import invariant from 'tiny-invariant';
|
||||
|
||||
import cadastreLayers from './layers/cadastre';
|
||||
import cadastreLayers from './layers/cadastre.json';
|
||||
|
||||
function ignServiceURL(layer: string, style: string, format = 'image/png') {
|
||||
const url = `https://data.geopf.fr/wmts`;
|
||||
|
@ -163,7 +168,10 @@ function buildSources() {
|
|||
);
|
||||
}
|
||||
|
||||
function rasterSource(tiles: string[], attribution: string): RasterSource {
|
||||
function rasterSource(
|
||||
tiles: string[],
|
||||
attribution: string
|
||||
): RasterSourceSpecification {
|
||||
return {
|
||||
type: 'raster',
|
||||
tiles,
|
||||
|
@ -174,7 +182,10 @@ function rasterSource(tiles: string[], attribution: string): RasterSource {
|
|||
};
|
||||
}
|
||||
|
||||
function rasterLayer(source: string, opacity: number): RasterLayer {
|
||||
function rasterLayer(
|
||||
source: string,
|
||||
opacity: number
|
||||
): RasterLayerSpecification {
|
||||
return {
|
||||
id: source,
|
||||
source,
|
||||
|
@ -186,14 +197,14 @@ function rasterLayer(source: string, opacity: number): RasterLayer {
|
|||
export function buildOptionalLayers(
|
||||
ids: string[],
|
||||
opacity: Record<string, number>
|
||||
): AnyLayer[] {
|
||||
): LayerSpecification[] {
|
||||
return OPTIONAL_LAYERS.filter(({ id }) => ids.includes(id))
|
||||
.flatMap(({ layers, id }) =>
|
||||
layers.map(([, code]) => [code, opacity[id] / 100] as const)
|
||||
)
|
||||
.flatMap(([code, opacity]) =>
|
||||
code === 'CADASTRE'
|
||||
? cadastreLayers
|
||||
? (cadastreLayers as LayerSpecification[])
|
||||
: [rasterLayer(getLayerCode(code), opacity)]
|
||||
);
|
||||
}
|
||||
|
@ -210,9 +221,9 @@ function getLayerCode(code: string) {
|
|||
return code.toLowerCase().replace(/\./g, '-');
|
||||
}
|
||||
|
||||
export default {
|
||||
export const style: StyleSpecification = {
|
||||
version: 8,
|
||||
metadat: {
|
||||
metadata: {
|
||||
'mapbox:autocomposite': false,
|
||||
'mapbox:groups': {
|
||||
1444849242106.713: { collapsed: false, name: 'Places' },
|
||||
|
@ -257,5 +268,6 @@ export default {
|
|||
...buildSources()
|
||||
},
|
||||
sprite: 'https://openmaptiles.github.io/osm-bright-gl-style/sprite',
|
||||
glyphs: 'https://openmaptiles.geo.data.gouv.fr/fonts/{fontstack}/{range}.pbf'
|
||||
} as Style;
|
||||
glyphs: 'https://openmaptiles.geo.data.gouv.fr/fonts/{fontstack}/{range}.pbf',
|
||||
layers: []
|
||||
};
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
import type { Style } from 'maplibre-gl';
|
||||
import type { LayerSpecification, StyleSpecification } from 'maplibre-gl';
|
||||
|
||||
import baseStyle, { buildOptionalLayers, getLayerName, NBS } from './base';
|
||||
import orthoStyle from './layers/ortho';
|
||||
import vectorStyle from './layers/vector';
|
||||
import ignLayers from './layers/ign';
|
||||
import {
|
||||
style as baseStyle,
|
||||
buildOptionalLayers,
|
||||
getLayerName,
|
||||
NBS
|
||||
} from './base';
|
||||
import ignLayers from './layers/ign.json';
|
||||
import orthoLayers from './layers/ortho.json';
|
||||
import vectorLayers from './layers/vector.json';
|
||||
|
||||
export { getLayerName, NBS };
|
||||
|
||||
|
@ -21,20 +26,20 @@ export function getMapStyle(
|
|||
id: string,
|
||||
layers: string[],
|
||||
opacity: Record<string, number>
|
||||
): Style & { id: string } {
|
||||
): StyleSpecification & { id: string } {
|
||||
const style = { ...baseStyle, id };
|
||||
|
||||
switch (id) {
|
||||
case 'ortho':
|
||||
style.layers = orthoStyle;
|
||||
style.layers = orthoLayers as LayerSpecification[];
|
||||
style.name = 'Photographies aériennes';
|
||||
break;
|
||||
case 'vector':
|
||||
style.layers = vectorStyle;
|
||||
style.layers = vectorLayers as LayerSpecification[];
|
||||
style.name = 'Carte OSM';
|
||||
break;
|
||||
case 'ign':
|
||||
style.layers = ignLayers;
|
||||
style.layers = ignLayers as LayerSpecification[];
|
||||
style.name = 'Carte IGN';
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
[
|
||||
{
|
||||
"id": "batiments-line",
|
||||
"type": "line",
|
||||
"source": "cadastre",
|
||||
"source-layer": "batiments",
|
||||
"minzoom": 16,
|
||||
"maxzoom": 22,
|
||||
"layout": { "visibility": "visible" },
|
||||
"paint": {
|
||||
"line-opacity": 1,
|
||||
"line-color": "rgba(0, 0, 0, 1)",
|
||||
"line-width": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "batiments-fill",
|
||||
"type": "fill",
|
||||
"source": "cadastre",
|
||||
"source-layer": "batiments",
|
||||
"layout": { "visibility": "visible" },
|
||||
"paint": {
|
||||
"fill-color": "rgba(150, 150, 150, 1)",
|
||||
"fill-opacity": {
|
||||
"stops": [
|
||||
[16, 0],
|
||||
[17, 0.6]
|
||||
]
|
||||
},
|
||||
"fill-antialias": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "parcelles",
|
||||
"type": "line",
|
||||
"source": "cadastre",
|
||||
"source-layer": "parcelles",
|
||||
"minzoom": 15.5,
|
||||
"maxzoom": 24,
|
||||
"layout": {
|
||||
"visibility": "visible",
|
||||
"line-cap": "butt",
|
||||
"line-join": "miter",
|
||||
"line-miter-limit": 2
|
||||
},
|
||||
"paint": {
|
||||
"line-color": "rgba(255, 255, 255, 1)",
|
||||
"line-opacity": 0.8,
|
||||
"line-width": {
|
||||
"stops": [
|
||||
[16, 1.5],
|
||||
[17, 2]
|
||||
]
|
||||
},
|
||||
"line-offset": 0,
|
||||
"line-blur": 0,
|
||||
"line-translate": [0, 1],
|
||||
"line-dasharray": [1],
|
||||
"line-gap-width": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "parcelles-fill",
|
||||
"type": "fill",
|
||||
"source": "cadastre",
|
||||
"source-layer": "parcelles",
|
||||
"layout": {
|
||||
"visibility": "visible"
|
||||
},
|
||||
"paint": {
|
||||
"fill-color": "rgba(129, 123, 0, 1)",
|
||||
"fill-opacity": [
|
||||
"case",
|
||||
["boolean", ["feature-state", "hover"], false],
|
||||
0.7,
|
||||
0.1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "parcelle-highlighted",
|
||||
"type": "fill",
|
||||
"source": "cadastre",
|
||||
"source-layer": "parcelles",
|
||||
"filter": ["in", "id", ""],
|
||||
"paint": {
|
||||
"fill-color": "rgba(1, 129, 0, 1)",
|
||||
"fill-opacity": 0.7
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "sections",
|
||||
"type": "line",
|
||||
"source": "cadastre",
|
||||
"source-layer": "sections",
|
||||
"minzoom": 12,
|
||||
"layout": { "visibility": "visible" },
|
||||
"paint": {
|
||||
"line-color": "rgba(0, 0, 0, 1)",
|
||||
"line-opacity": 0.7,
|
||||
"line-width": 2,
|
||||
"line-dasharray": [3, 3],
|
||||
"line-translate": [0, 0]
|
||||
}
|
||||
}
|
||||
]
|
|
@ -1,110 +0,0 @@
|
|||
import type { AnyLayer } from 'maplibre-gl';
|
||||
|
||||
const layers: AnyLayer[] = [
|
||||
{
|
||||
id: 'batiments-line',
|
||||
type: 'line',
|
||||
source: 'cadastre',
|
||||
'source-layer': 'batiments',
|
||||
minzoom: 16,
|
||||
maxzoom: 22,
|
||||
layout: { visibility: 'visible' },
|
||||
paint: {
|
||||
'line-opacity': 1,
|
||||
'line-color': 'rgba(0, 0, 0, 1)',
|
||||
'line-width': 1
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'batiments-fill',
|
||||
type: 'fill',
|
||||
source: 'cadastre',
|
||||
'source-layer': 'batiments',
|
||||
layout: { visibility: 'visible' },
|
||||
paint: {
|
||||
'fill-color': 'rgba(150, 150, 150, 1)',
|
||||
'fill-opacity': {
|
||||
stops: [
|
||||
[16, 0],
|
||||
[17, 0.6]
|
||||
]
|
||||
},
|
||||
'fill-antialias': true
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'parcelles',
|
||||
type: 'line',
|
||||
source: 'cadastre',
|
||||
'source-layer': 'parcelles',
|
||||
minzoom: 15.5,
|
||||
maxzoom: 24,
|
||||
layout: {
|
||||
visibility: 'visible',
|
||||
'line-cap': 'butt',
|
||||
'line-join': 'miter',
|
||||
'line-miter-limit': 2
|
||||
},
|
||||
paint: {
|
||||
'line-color': 'rgba(255, 255, 255, 1)',
|
||||
'line-opacity': 0.8,
|
||||
'line-width': {
|
||||
stops: [
|
||||
[16, 1.5],
|
||||
[17, 2]
|
||||
]
|
||||
},
|
||||
'line-offset': 0,
|
||||
'line-blur': 0,
|
||||
'line-translate': [0, 1],
|
||||
'line-dasharray': [1],
|
||||
'line-gap-width': 0
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'parcelles-fill',
|
||||
type: 'fill',
|
||||
source: 'cadastre',
|
||||
'source-layer': 'parcelles',
|
||||
layout: {
|
||||
visibility: 'visible'
|
||||
},
|
||||
paint: {
|
||||
'fill-color': 'rgba(129, 123, 0, 1)',
|
||||
'fill-opacity': [
|
||||
'case',
|
||||
['boolean', ['feature-state', 'hover'], false],
|
||||
0.7,
|
||||
0.1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'parcelle-highlighted',
|
||||
type: 'fill',
|
||||
source: 'cadastre',
|
||||
'source-layer': 'parcelles',
|
||||
filter: ['in', 'id', ''],
|
||||
paint: {
|
||||
'fill-color': 'rgba(1, 129, 0, 1)',
|
||||
'fill-opacity': 0.7
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'sections',
|
||||
type: 'line',
|
||||
source: 'cadastre',
|
||||
'source-layer': 'sections',
|
||||
minzoom: 12,
|
||||
layout: { visibility: 'visible' },
|
||||
paint: {
|
||||
'line-color': 'rgba(0, 0, 0, 1)',
|
||||
'line-opacity': 0.7,
|
||||
'line-width': 2,
|
||||
'line-dasharray': [3, 3],
|
||||
'line-translate': [0, 0]
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export default layers;
|
|
@ -0,0 +1,8 @@
|
|||
[
|
||||
{
|
||||
"id": "ign",
|
||||
"source": "plan-ign",
|
||||
"type": "raster",
|
||||
"paint": { "raster-resampling": "linear" }
|
||||
}
|
||||
]
|
|
@ -1,12 +0,0 @@
|
|||
import type { RasterLayer } from 'maplibre-gl';
|
||||
|
||||
const layers: RasterLayer[] = [
|
||||
{
|
||||
id: 'ign',
|
||||
source: 'plan-ign',
|
||||
type: 'raster',
|
||||
paint: { 'raster-resampling': 'linear' }
|
||||
}
|
||||
];
|
||||
|
||||
export default layers;
|
2647
app/javascript/components/shared/maplibre/styles/layers/ortho.json
Normal file
2647
app/javascript/components/shared/maplibre/styles/layers/ortho.json
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
2866
app/javascript/components/shared/maplibre/styles/layers/vector.json
Normal file
2866
app/javascript/components/shared/maplibre/styles/layers/vector.json
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -40,3 +40,4 @@
|
|||
@import '@gouvfr/dsfr/dist/component/accordion/accordion.css';
|
||||
@import '@gouvfr/dsfr/dist/component/tab/tab.css';
|
||||
@import '@gouvfr/dsfr/dist/component/tooltip/tooltip.css';
|
||||
@import '@gouvfr/dsfr/dist/component/range/range.css';
|
||||
|
|
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
37
package.json
37
package.json
|
@ -10,11 +10,10 @@
|
|||
"@gouvfr/dsfr": "^1.11.2",
|
||||
"@graphiql/plugin-explorer": "^3.1.0",
|
||||
"@graphiql/toolkit": "^0.9.1",
|
||||
"@headlessui/react": "^1.6.6",
|
||||
"@heroicons/react": "^1.0.6",
|
||||
"@hotwired/stimulus": "^3.2.2",
|
||||
"@hotwired/turbo": "^7.3.0",
|
||||
"@mapbox/mapbox-gl-draw": "^1.3.0",
|
||||
"@mapbox/mapbox-gl-draw": "^1.4.3",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@rails/actiontext": "^7.1.3-4",
|
||||
"@rails/activestorage": "^7.1.3-4",
|
||||
|
@ -52,14 +51,14 @@
|
|||
"graphql": "^16.9.0",
|
||||
"highcharts": "^10.3.3",
|
||||
"lightgallery": "^2.7.2",
|
||||
"maplibre-gl": "^1.15.2",
|
||||
"maplibre-gl": "^4.5.0",
|
||||
"match-sorter": "^6.3.4",
|
||||
"patch-package": "^8.0.0",
|
||||
"react": "^18.3.1",
|
||||
"react-aria-components": "^1.3.1",
|
||||
"react-coordinate-input": "^1.0.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-popper": "^2.3.0",
|
||||
"react-fast-compare": "^3.2.2",
|
||||
"react-use-event-hook": "^0.9.6",
|
||||
"spectaql": "^3.0.1",
|
||||
"stimulus-use": "^0.52.2",
|
||||
|
@ -81,7 +80,7 @@
|
|||
"@types/debounce": "^1.2.4",
|
||||
"@types/geojson": "^7946.0.14",
|
||||
"@types/is-hotkey": "^0.1.10",
|
||||
"@types/mapbox__mapbox-gl-draw": "^1.2.5",
|
||||
"@types/mapbox__mapbox-gl-draw": "^1.4.6",
|
||||
"@types/rails__activestorage": "^7.1.1",
|
||||
"@types/rails__ujs": "^6.0.4",
|
||||
"@types/react": "^18.3.3",
|
||||
|
@ -137,7 +136,10 @@
|
|||
"process": true,
|
||||
"gon": true
|
||||
},
|
||||
"plugins": ["prettier", "react-hooks"],
|
||||
"plugins": [
|
||||
"prettier",
|
||||
"react-hooks"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"prettier",
|
||||
|
@ -156,16 +158,29 @@
|
|||
"react/no-deprecated": "off"
|
||||
},
|
||||
"settings": {
|
||||
"react": { "version": "detect" }
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": [".eslintrc.js", "vite.config.ts", "postcss.config.js"],
|
||||
"env": { "node": true }
|
||||
"files": [
|
||||
".eslintrc.js",
|
||||
"vite.config.ts",
|
||||
"postcss.config.js"
|
||||
],
|
||||
"env": {
|
||||
"node": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["**/*.ts", "**/*.tsx"],
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"files": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
],
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
|
|
Loading…
Reference in a new issue