Merge pull request #10845 from tchak/update-maplibre

chore(maplibre): update
This commit is contained in:
Paul Chavard 2024-09-23 12:53:34 +00:00 committed by GitHub
commit 0321226130
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 5936 additions and 5826 deletions

View file

@ -1,6 +1,4 @@
@import "colors"; @import "colors";
@import "constants";
.areas { .areas {
margin-bottom: 10px; margin-bottom: 10px;
@ -10,56 +8,49 @@
} }
} }
.map-style-control { .ds-ctrl button {
position: absolute; color: $dark-grey;
bottom: 4px;
left: 10px;
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;
}
}
}
}
.cadastres-selection-control {
z-index: 1;
position: absolute;
top: 135px;
left: 10px;
button {
&.on, &.on,
&:hover { &:hover {
background-color: rgba(0, 0, 0, 0.05); background-color: rgba(0, 0, 0, 0.05);
} }
} }
.react-aria-popover {
&[data-placement='top'] {
--origin: translateY(8px);
}
&[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;
}
} }

View file

@ -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 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 { import {
useEvent, useEvent,
useMapEvent, useMapEvent,
@ -18,15 +20,31 @@ export function CadastreLayer({
featureCollection, featureCollection,
createFeatures, createFeatures,
deleteFeatures, deleteFeatures,
toggle,
enabled enabled
}: { }: {
featureCollection: FeatureCollection; featureCollection: FeatureCollection;
createFeatures: CreateFeatures; createFeatures: CreateFeatures;
deleteFeatures: DeleteFeatures; deleteFeatures: DeleteFeatures;
toggle: () => void;
enabled: boolean; enabled: boolean;
}) { }) {
const map = useMapLibre(); const map = useMapLibre();
const selectedCadastresRef = useRef(new Set<string>()); 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( const highlightFeature = useCallback(
(cid: string, highlight: boolean) => { (cid: string, highlight: boolean) => {
@ -95,7 +113,35 @@ export function CadastreLayer({
useEvent('map:internal:cadastre:highlight', onHighlight); 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( function useCadastres(

View file

@ -1,5 +1,5 @@
import { useCallback, useRef, useEffect } from 'react'; 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 DrawControl from '@mapbox/mapbox-gl-draw';
import type { FeatureCollection, Feature, Point } from 'geojson'; 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. // We use mapbox-draw plugin with maplibre. They are compatible but types are not.
// eslint-disable-next-line @typescript-eslint/no-explicit-any // 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( draw.set(
filterFeatureCollection(featureCollection, SOURCE_SELECTION_UTILISATEUR) filterFeatureCollection(featureCollection, SOURCE_SELECTION_UTILISATEUR)
); );
drawRef.current = draw; drawRef.current = draw;
for (const [selector, translation] of translations) { patchDrawControl();
const element = document.querySelector(selector);
if (element) {
element.setAttribute('title', translation);
}
}
} }
return () => { return () => {
@ -228,3 +224,15 @@ const translations = [
['.mapbox-gl-draw_point', 'Ajouter un point'], ['.mapbox-gl-draw_point', 'Ajouter un point'],
['.mapbox-gl-draw_trash', 'Supprimer'] ['.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);
}
}
});
}

View file

@ -1,6 +1,4 @@
import { useState } from 'react'; 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 '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css';
import type { FeatureCollection } from 'geojson'; import type { FeatureCollection } from 'geojson';
@ -51,25 +49,12 @@ export default function MapEditor({
enabled={!cadastreEnabled} enabled={!cadastreEnabled}
/> />
{options.layers.includes('cadastres') ? ( {options.layers.includes('cadastres') ? (
<>
<CadastreLayer <CadastreLayer
featureCollection={featureCollection} featureCollection={featureCollection}
{...actions} {...actions}
toggle={() => setCadastreEnabled((enabled) => !enabled)}
enabled={cadastreEnabled} 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>
</>
) : null} ) : null}
</MapLibre> </MapLibre>
<PointInput featureCollection={featureCollection} /> <PointInput featureCollection={featureCollection} />

View file

@ -1,4 +1,3 @@
import 'maplibre-gl/dist/maplibre-gl.css';
import type { FeatureCollection } from 'geojson'; import type { FeatureCollection } from 'geojson';
import { MapLibre } from '../shared/maplibre/MapLibre'; import { MapLibre } from '../shared/maplibre/MapLibre';

View file

@ -8,13 +8,15 @@ import {
createContext, createContext,
useCallback useCallback
} from 'react'; } from 'react';
import maplibre, { Map, NavigationControl } from 'maplibre-gl'; import { createPortal } from 'react-dom';
import type { Style } from 'maplibre-gl'; 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 invariant from 'tiny-invariant';
import { useStyle, useElementVisible } from './hooks'; import { useStyle, useElementVisible } from './hooks';
import { StyleControl } from './StyleControl'; import { StyleSwitch } from './StyleControl';
const Context = createContext<{ map?: Map | null }>({}); const Context = createContext<{ map?: Map | null }>({});
@ -30,16 +32,15 @@ export function useMapLibre() {
} }
export function MapLibre({ children, layers }: MapLibreProps) { export function MapLibre({ children, layers }: MapLibreProps) {
const isSupported = useMemo( const isSupported = useMemo(() => isWebglSupported(), []);
() => maplibre.supported({ failIfMajorPerformanceCaveat: true }) && !isIE(),
[]
);
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const visible = useElementVisible(containerRef); const visible = useElementVisible(containerRef);
const [map, setMap] = useState<Map | null>(); const [map, setMap] = useState<Map | null>();
const [styleControlElement, setStyleControlElement] =
useState<HTMLElement | null>(null);
const onStyleChange = useCallback( const onStyleChange = useCallback(
(style: Style) => { (style: StyleSpecification) => {
if (map) { if (map) {
map.setStyle(style); map.setStyle(style);
} }
@ -56,8 +57,11 @@ export function MapLibre({ children, layers }: MapLibreProps) {
style style
}); });
map.addControl(new NavigationControl({}), 'top-right'); map.addControl(new NavigationControl({}), 'top-right');
const styleControl = new ReactControl();
map.addControl(styleControl, 'bottom-left');
map.on('load', () => { map.on('load', () => {
setMap(map); setMap(map);
setStyleControlElement(styleControl.container);
}); });
} }
}, [map, style, visible, isSupported]); }, [map, style, visible, isSupported]);
@ -91,16 +95,54 @@ export function MapLibre({ children, layers }: MapLibreProps) {
return ( return (
<Context.Provider value={{ map }}> <Context.Provider value={{ map }}>
<div ref={containerRef} style={{ height: '500px' }}> <div ref={containerRef} style={{ height: '500px' }}>
<StyleControl styleId={style.id} {...mapStyleProps} /> {styleControlElement != null
? createPortal(
<StyleSwitch styleId={style.id} {...mapStyleProps} />,
styleControlElement
)
: null}
{map ? children : null} {map ? children : null}
</div> </div>
</Context.Provider> </Context.Provider>
); );
} }
function isIE() { function isWebglSupported() {
const ua = window.navigator.userAgent; if (window.WebGLRenderingContext) {
const msie = ua.indexOf('MSIE '); const canvas = document.createElement('canvas');
const trident = ua.indexOf('Trident/'); try {
return msie > 0 || trident > 0; // 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;
}
} }

View file

@ -1,6 +1,5 @@
import { useState, useId } from 'react'; import { useId, useRef, useEffect } from 'react';
import { Popover, RadioGroup } from '@headlessui/react'; import { Button, Dialog, DialogTrigger, Popover } from 'react-aria-components';
import { usePopper } from 'react-popper';
import { MapIcon } from '@heroicons/react/outline'; import { MapIcon } from '@heroicons/react/outline';
import { Slider } from '@reach/slider'; import { Slider } from '@reach/slider';
import '@reach/slider/styles.css'; import '@reach/slider/styles.css';
@ -13,7 +12,7 @@ const STYLES = {
ign: 'Carte IGN' ign: 'Carte IGN'
}; };
export function StyleControl({ export function StyleSwitch({
styleId, styleId,
layers, layers,
setStyle, setStyle,
@ -26,81 +25,69 @@ export function StyleControl({
setLayerEnabled: (layer: string, enabled: boolean) => void; setLayerEnabled: (layer: string, enabled: boolean) => void;
setLayerOpacity: (layer: string, opacity: number) => 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( const configurableLayers = Object.entries(layers).filter(
([, { configurable }]) => configurable ([, { configurable }]) => configurable
); );
const mapId = useId(); const mapId = useId();
const buttonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (buttonRef.current) {
buttonRef.current.title = 'Sélectionner les couches cartographiques';
}
}, []);
return ( return (
<div <DialogTrigger>
className="form map-style-control mapboxgl-ctrl-group" <Button ref={buttonRef}>
style={{ zIndex: 10 }}
>
<Popover>
<Popover.Button
ref={setButtonElement}
className="map-style-button"
title="Sélectionner les couches cartographiques"
>
<MapIcon className="icon-size" /> <MapIcon className="icon-size" />
</Popover.Button> </Button>
<Popover.Panel <Popover className="react-aria-popover">
className="flex map-style-panel mapboxgl-ctrl-group" <Dialog className="fr-modal__body">
ref={setPanelElement} <form
style={styles.popper} className="fr-modal__content flex m-2"
{...attributes.popper} onSubmit={(event) => event.preventDefault()}
>
<RadioGroup
value={styleId}
onChange={setStyle}
className="styles-list"
as="ul"
> >
<div className="fr-fieldset">
{Object.entries(STYLES).map(([style, title]) => ( {Object.entries(STYLES).map(([style, title]) => (
<RadioGroup.Option <div className="fr-fieldset__element" key={style}>
key={style} <div className="fr-radio-group">
value={style}
as="li"
className="flex"
>
{({ checked }) => (
<>
<input <input
id={`${mapId}-${style}`}
value={style}
type="radio" type="radio"
key={`${style}-${checked}`}
defaultChecked={checked}
name="map-style" name="map-style"
className="m-0 p-0 mr-1" defaultValue={style}
checked={styleId == style}
onChange={(event) => {
setStyle(event.target.value);
}}
/> />
<RadioGroup.Label> <label htmlFor={`${mapId}-${style}`} className="fr-label">
{title.replace(/\s/g, ' ')} {title.replace(/\s/g, ' ')}
</RadioGroup.Label> </label>
</> </div>
)} </div>
</RadioGroup.Option>
))} ))}
</RadioGroup> </div>
{configurableLayers.length ? ( {configurableLayers.length ? (
<ul className="layers-list"> <div className="fr-fieldset__element">
{configurableLayers.map(([layer, { enabled, opacity, name }]) => ( {configurableLayers.map(
<li key={layer}> ([layer, { enabled, opacity, name }]) => (
<div className="flex mb-1"> <div key={layer} className="fr-fieldset__element">
<div className="fr-checkbox-group">
<input <input
id={`${mapId}-${layer}`} id={`${mapId}-${layer}`}
className="m-0 p-0 mr-1"
type="checkbox" type="checkbox"
checked={enabled} checked={enabled}
onChange={(event) => { onChange={(event) => {
setLayerEnabled(layer, event.target.checked); setLayerEnabled(layer, event.target.checked);
}} }}
/> />
<label className="m-0" htmlFor={`${mapId}-${layer}`}> <label
className="fr-label"
htmlFor={`${mapId}-${layer}`}
>
{name} {name}
</label> </label>
</div> </div>
@ -112,7 +99,7 @@ export function StyleControl({
onChange={(value) => { onChange={(value) => {
setLayerOpacity(layer, value); setLayerOpacity(layer, value);
}} }}
className="mb-1" className="fr-range fr-range--sm mt-1"
title={`Réglage de lopacité de la couche «${NBS}${name}${NBS}»`} title={`Réglage de lopacité de la couche «${NBS}${name}${NBS}»`}
getAriaLabel={() => getAriaLabel={() =>
`Réglage de lopacité de la couche «${NBS}${name}${NBS}»` `Réglage de lopacité de la couche «${NBS}${name}${NBS}»`
@ -121,12 +108,14 @@ export function StyleControl({
`Lopacité de la couche «${NBS}${name}${NBS}» est à ${value}${NBS}%` `Lopacité de la couche «${NBS}${name}${NBS}» est à ${value}${NBS}%`
} }
/> />
</li>
))}
</ul>
) : null}
</Popover.Panel>
</Popover>
</div> </div>
)
)}
</div>
) : null}
</form>
</Dialog>
</Popover>
</DialogTrigger>
); );
} }

View file

@ -9,7 +9,7 @@ import type {
LngLatBoundsLike, LngLatBoundsLike,
LngLat, LngLat,
MapLayerEventType, MapLayerEventType,
Style, StyleSpecification,
LngLatLike LngLatLike
} from 'maplibre-gl'; } from 'maplibre-gl';
import type { Feature, Geometry } from 'geojson'; import type { Feature, Geometry } from 'geojson';
@ -104,7 +104,7 @@ function optionalLayersMap(optionalLayers: string[]): LayersMap {
export function useStyle( export function useStyle(
optionalLayers: string[], optionalLayers: string[],
onStyleChange: (style: Style) => void onStyleChange: (style: StyleSpecification) => void
) { ) {
const [styleId, setStyle] = useState('ortho'); const [styleId, setStyle] = useState('ortho');
const [layers, setLayers] = useState(() => optionalLayersMap(optionalLayers)); const [layers, setLayers] = useState(() => optionalLayersMap(optionalLayers));

View file

@ -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 invariant from 'tiny-invariant';
import cadastreLayers from './layers/cadastre'; import cadastreLayers from './layers/cadastre.json';
function ignServiceURL(layer: string, style: string, format = 'image/png') { function ignServiceURL(layer: string, style: string, format = 'image/png') {
const url = `https://data.geopf.fr/wmts`; 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 { return {
type: 'raster', type: 'raster',
tiles, 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 { return {
id: source, id: source,
source, source,
@ -186,14 +197,14 @@ function rasterLayer(source: string, opacity: number): RasterLayer {
export function buildOptionalLayers( export function buildOptionalLayers(
ids: string[], ids: string[],
opacity: Record<string, number> opacity: Record<string, number>
): AnyLayer[] { ): LayerSpecification[] {
return OPTIONAL_LAYERS.filter(({ id }) => ids.includes(id)) return OPTIONAL_LAYERS.filter(({ id }) => ids.includes(id))
.flatMap(({ layers, id }) => .flatMap(({ layers, id }) =>
layers.map(([, code]) => [code, opacity[id] / 100] as const) layers.map(([, code]) => [code, opacity[id] / 100] as const)
) )
.flatMap(([code, opacity]) => .flatMap(([code, opacity]) =>
code === 'CADASTRE' code === 'CADASTRE'
? cadastreLayers ? (cadastreLayers as LayerSpecification[])
: [rasterLayer(getLayerCode(code), opacity)] : [rasterLayer(getLayerCode(code), opacity)]
); );
} }
@ -210,9 +221,9 @@ function getLayerCode(code: string) {
return code.toLowerCase().replace(/\./g, '-'); return code.toLowerCase().replace(/\./g, '-');
} }
export default { export const style: StyleSpecification = {
version: 8, version: 8,
metadat: { metadata: {
'mapbox:autocomposite': false, 'mapbox:autocomposite': false,
'mapbox:groups': { 'mapbox:groups': {
1444849242106.713: { collapsed: false, name: 'Places' }, 1444849242106.713: { collapsed: false, name: 'Places' },
@ -257,5 +268,6 @@ export default {
...buildSources() ...buildSources()
}, },
sprite: 'https://openmaptiles.github.io/osm-bright-gl-style/sprite', sprite: 'https://openmaptiles.github.io/osm-bright-gl-style/sprite',
glyphs: 'https://openmaptiles.geo.data.gouv.fr/fonts/{fontstack}/{range}.pbf' glyphs: 'https://openmaptiles.geo.data.gouv.fr/fonts/{fontstack}/{range}.pbf',
} as Style; layers: []
};

View file

@ -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 {
import orthoStyle from './layers/ortho'; style as baseStyle,
import vectorStyle from './layers/vector'; buildOptionalLayers,
import ignLayers from './layers/ign'; 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 }; export { getLayerName, NBS };
@ -21,20 +26,20 @@ export function getMapStyle(
id: string, id: string,
layers: string[], layers: string[],
opacity: Record<string, number> opacity: Record<string, number>
): Style & { id: string } { ): StyleSpecification & { id: string } {
const style = { ...baseStyle, id }; const style = { ...baseStyle, id };
switch (id) { switch (id) {
case 'ortho': case 'ortho':
style.layers = orthoStyle; style.layers = orthoLayers as LayerSpecification[];
style.name = 'Photographies aériennes'; style.name = 'Photographies aériennes';
break; break;
case 'vector': case 'vector':
style.layers = vectorStyle; style.layers = vectorLayers as LayerSpecification[];
style.name = 'Carte OSM'; style.name = 'Carte OSM';
break; break;
case 'ign': case 'ign':
style.layers = ignLayers; style.layers = ignLayers as LayerSpecification[];
style.name = 'Carte IGN'; style.name = 'Carte IGN';
break; break;
} }

View file

@ -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]
}
}
]

View file

@ -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;

View file

@ -0,0 +1,8 @@
[
{
"id": "ign",
"source": "plan-ign",
"type": "raster",
"paint": { "raster-resampling": "linear" }
}
]

View file

@ -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;

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -40,3 +40,4 @@
@import '@gouvfr/dsfr/dist/component/accordion/accordion.css'; @import '@gouvfr/dsfr/dist/component/accordion/accordion.css';
@import '@gouvfr/dsfr/dist/component/tab/tab.css'; @import '@gouvfr/dsfr/dist/component/tab/tab.css';
@import '@gouvfr/dsfr/dist/component/tooltip/tooltip.css'; @import '@gouvfr/dsfr/dist/component/tooltip/tooltip.css';
@import '@gouvfr/dsfr/dist/component/range/range.css';

BIN
bun.lockb

Binary file not shown.

View file

@ -10,11 +10,10 @@
"@gouvfr/dsfr": "^1.11.2", "@gouvfr/dsfr": "^1.11.2",
"@graphiql/plugin-explorer": "^3.1.0", "@graphiql/plugin-explorer": "^3.1.0",
"@graphiql/toolkit": "^0.9.1", "@graphiql/toolkit": "^0.9.1",
"@headlessui/react": "^1.6.6",
"@heroicons/react": "^1.0.6", "@heroicons/react": "^1.0.6",
"@hotwired/stimulus": "^3.2.2", "@hotwired/stimulus": "^3.2.2",
"@hotwired/turbo": "^7.3.0", "@hotwired/turbo": "^7.3.0",
"@mapbox/mapbox-gl-draw": "^1.3.0", "@mapbox/mapbox-gl-draw": "^1.4.3",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"@rails/actiontext": "^7.1.3-4", "@rails/actiontext": "^7.1.3-4",
"@rails/activestorage": "^7.1.3-4", "@rails/activestorage": "^7.1.3-4",
@ -52,14 +51,14 @@
"graphql": "^16.9.0", "graphql": "^16.9.0",
"highcharts": "^10.3.3", "highcharts": "^10.3.3",
"lightgallery": "^2.7.2", "lightgallery": "^2.7.2",
"maplibre-gl": "^1.15.2", "maplibre-gl": "^4.5.0",
"match-sorter": "^6.3.4", "match-sorter": "^6.3.4",
"patch-package": "^8.0.0", "patch-package": "^8.0.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-aria-components": "^1.3.1", "react-aria-components": "^1.3.1",
"react-coordinate-input": "^1.0.0", "react-coordinate-input": "^1.0.0",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-popper": "^2.3.0", "react-fast-compare": "^3.2.2",
"react-use-event-hook": "^0.9.6", "react-use-event-hook": "^0.9.6",
"spectaql": "^3.0.1", "spectaql": "^3.0.1",
"stimulus-use": "^0.52.2", "stimulus-use": "^0.52.2",
@ -81,7 +80,7 @@
"@types/debounce": "^1.2.4", "@types/debounce": "^1.2.4",
"@types/geojson": "^7946.0.14", "@types/geojson": "^7946.0.14",
"@types/is-hotkey": "^0.1.10", "@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__activestorage": "^7.1.1",
"@types/rails__ujs": "^6.0.4", "@types/rails__ujs": "^6.0.4",
"@types/react": "^18.3.3", "@types/react": "^18.3.3",
@ -137,7 +136,10 @@
"process": true, "process": true,
"gon": true "gon": true
}, },
"plugins": ["prettier", "react-hooks"], "plugins": [
"prettier",
"react-hooks"
],
"extends": [ "extends": [
"eslint:recommended", "eslint:recommended",
"prettier", "prettier",
@ -156,16 +158,29 @@
"react/no-deprecated": "off" "react/no-deprecated": "off"
}, },
"settings": { "settings": {
"react": { "version": "detect" } "react": {
"version": "detect"
}
}, },
"overrides": [ "overrides": [
{ {
"files": [".eslintrc.js", "vite.config.ts", "postcss.config.js"], "files": [
"env": { "node": true } ".eslintrc.js",
"vite.config.ts",
"postcss.config.js"
],
"env": {
"node": true
}
}, },
{ {
"files": ["**/*.ts", "**/*.tsx"], "files": [
"plugins": ["@typescript-eslint"], "**/*.ts",
"**/*.tsx"
],
"plugins": [
"@typescript-eslint"
],
"extends": [ "extends": [
"eslint:recommended", "eslint:recommended",
"plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended",