Add support for Geometry type, add Map Layout & Interface (#5684)

* Added map layout

* Cleanup and bug fixes

* Removed package-lock

* Cleanup and fixes

* Small fix

* Added back package-lock

* Saved camera, autofitting option, bug fixes

* Refactor and ui improvements

* Improvements

* Added seled mode

* Removed unused dependency

* Changed selection behaviour, cleanup.

* update import and dependencies

* make custom style into drawer

* remove unused imports

* use lodash functions

* add popups

* allow header to become small

* reorganize settings

* add styling to popup

* change default template

* add projection option

* add basic map interface

* finish simple map

* add mapbox style

* support more mapbox layouts

* add api key option

* add mapbox backgrounds to layout

* warn when no api key is set

* fix for latest version

* Improved map layout and interface, bug fixes, refactoring.

.

.

* Added postgis geometry format, added marker icon shadow

* Made map buttons bigger and their icons thinner. Added transition to header bar.

* Bug fixes and error handling in map interface.

* Moved box-select control out of the map component. Removed material icons sprite and use addImage for marker support.

* Handle MultiGeometry -> Geometry interface error.

* Removed hardcoded styles. Added migrations for basemap column. Lots of refactoring.

Removed hardcoded styles. Added migrations for basemap column. Lots of refactoring.

* Fixed style reloading error. Added translations.

* Moved worker code to lib.

* Removed worker code. Prevent Mapbox from removing access_token from the URL.

* Refactoring.

* Change basemap selection to in-map dropdown for layout and interface.

* Touchscreen selection support and small fixes.

* Small change.

* Fixed unused imports.

* Added support for PostgreSQL identity column

* Renamed migration. Added crs translation.

* Only show fields using the map interface in the map layout.

* Removed logging.

* Reverted Dockerfile change.

* Improved crs support.

* Fixed translations.

* Check for schema identity before updating it.

* Fixed popup not updating on feature hover.

* Added feature hover styling. Fixed layer customization input. Added out of bounds error handling.

* Added geometry type and support for database native geometries.

* Fixed linting.

* Fixed layout.

* Fixed layout.

* Actually fixed linting

* Full support for native geometries
Fixed basemap input
Improved feature popup on hover
Locked interfaced support

* Fixed geometryType option not updating

* Bug fixes in interface

* Fixed crash when empty basemap settings. Fixed fitBounds option not updating.

* Added back storage type option. Improved interface behaviour.

* Dropped wkb because of vendor inconsistency with binary data

* Updated layout to match new geometry type. Fixed geojson payload transform.

* Added missing geometry_format attributes to local types.

* Fixed typos & refactoring

* Removed dependency on proj4

* Fix error when empty map interface options

* Set geometry SRID to 4326 when inserting into the database

* Add support for selectMode

* Fix error on initial source load

* Added geocoder, use GeoJSON for api i/o, removed geometry_format option, refactoring

* Added geometry intersects filter. Created geometry helper class.

* Fix error when null geometryOptions, added mapbox_key setting.

* Moved all geometry parsing/serializing into processGeometries in `payload.ts`. Fixed type errors.

* Migrate to Vue 3

* Use wellknown instead of wkx

* Fixed basemap selection.

* Added available operator for geometry type

* Added nintersects filter, fixed map interface for filter input

* Added intersects_bbox filter & bug fixes.

* Fixed icons rendering

* Fixed cursor icon in select mode

* Added geometry aggregate function

* Fixed geometry processing bug when imported from relational field.

* Fixed error with geocoder instanciation

* Removed @types/maplibre-gl dependency

* Removed fitViewToData options

* Merge remote-tracking branch 'upstream/main' into map-layout

* Fixed style and geometryType in map interface options

* Fixed style change on map interface.

* Improved fitViewToData behaviour

* Fixed type imports and previous merge conflict

* Fixed linting

* Added available operators

* Fix and merge migrations

* Remove outdated p-queue dep

* Fix get-schema column extract

* Replace pg with postgis for local debugging

* Re-add missing import

* Add mapbox as a basemap when key exists

* Remove unused tz flag

* Process delta in payloadservice

* Set default map, add limit number styling

* Default display template to just PK

* Tweak styling of error dialog

* Fix method usage in helpers

* Move sdo_geo to oracle section

* Remove extensions from ts config exclude

* Move geo types to shared, remove _Geometry

* Remove unused type

* Tiny Tweaks

* Remove fit to bounds option in favor of on

* Validate incoming intersects query

* Deepmap filter values

* Add GraphQL support

* No defaultValue for geometryType

* Resolve c

* Fix translations

Co-authored-by: Nitwel <nitwel@arcor.de>
Co-authored-by: Rijk van Zanten <rijkvanzanten@me.com>
This commit is contained in:
Oreille
2021-08-12 22:01:34 +02:00
committed by GitHub
parent 66d4c04121
commit 83e8814b2d
67 changed files with 4982 additions and 231 deletions

View File

@@ -7,7 +7,7 @@
</div>
<transition-expand>
<div v-show="active" class="fields">
<div v-if="active" class="fields">
<v-form
:initial-values="initialValues"
:fields="fieldsInSection"

View File

@@ -33,7 +33,7 @@ export default defineInterface({
description: '$t:interfaces.input-code.description',
icon: 'code',
component: InterfaceCode,
types: ['string', 'json', 'text'],
types: ['string', 'json', 'text', 'geometry'],
options: [
{
field: 'language',

View File

@@ -8,6 +8,6 @@ export default defineInterface({
description: '$t:interfaces.input.description',
icon: 'text_fields',
component: InterfaceInput,
types: ['string', 'uuid', 'bigInteger', 'integer', 'float', 'decimal'],
types: ['string', 'uuid', 'bigInteger', 'integer', 'float', 'decimal', 'geometry'],
options: Options,
});

View File

@@ -0,0 +1,14 @@
import { defineInterface } from '@directus/shared/utils';
import InterfaceMap from './map.vue';
import Options from './options.vue';
export default defineInterface({
id: 'map',
name: '$t:interfaces.map.map',
description: '$t:interfaces.map.description',
icon: 'map',
component: InterfaceMap,
types: ['geometry', 'json', 'string', 'text', 'binary', 'csv'],
options: Options,
recommendedDisplays: [],
});

View File

@@ -0,0 +1,449 @@
<template>
<div class="interface-map">
<div
ref="container"
class="map"
:class="{ loading: mapLoading, error: geometryParsingError || geometryOptionsError }"
/>
<div class="mapboxgl-ctrl-group mapboxgl-ctrl mapboxgl-ctrl-dropdown basemap-select">
<v-icon name="map" />
<v-select v-model="basemap" inline :items="basemaps.map((s) => ({ text: s.name, value: s.name }))" />
</div>
<transition name="fade">
<v-info
v-if="geometryOptionsError"
icon="error"
center
type="danger"
:title="t('interfaces.map.invalid_options')"
>
<v-notice type="danger" :icon="false">
{{ geometryOptionsError }}
</v-notice>
</v-info>
<v-info
v-else-if="geometryParsingError"
icon="error"
center
type="warning"
:title="t('layouts.map.invalid_geometry')"
>
<v-notice type="warning" :icon="false">
{{ geometryParsingError }}
</v-notice>
<template #append>
<v-card-actions>
<v-button small class="soft-reset" secondary @click="resetValue(false)">{{ t('continue') }}</v-button>
<v-button small class="hard-reset" @click="resetValue(true)">{{ t('reset') }}</v-button>
</v-card-actions>
</template>
</v-info>
</transition>
</div>
</template>
<script lang="ts">
import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css';
import 'maplibre-gl/dist/maplibre-gl.css';
import { defineComponent, onMounted, onUnmounted, PropType, ref, watch, toRefs, computed } from 'vue';
import {
LngLatBoundsLike,
AnimationOptions,
CameraOptions,
Map,
NavigationControl,
GeolocateControl,
IControl,
} from 'maplibre-gl';
import MapboxDraw from '@mapbox/mapbox-gl-draw';
// @ts-ignore
import StaticMode from '@mapbox/mapbox-gl-draw-static-mode';
import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder';
import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css';
import { ButtonControl } from '@/utils/geometry/controls';
import { Geometry } from 'geojson';
import { flatten, getBBox, getParser, getSerializer, getGeometryFormatForType } from '@/utils/geometry';
import { GeoJSONParser, GeoJSONSerializer, SimpleGeometry, MultiGeometry } from '@directus/shared/types';
import getSetting from '@/utils/get-setting';
import { snakeCase, isEqual } from 'lodash';
import styles from './style';
import { Field, GeometryType, GeometryFormat } from '@directus/shared/types';
import { useI18n } from 'vue-i18n';
import { TranslateResult } from 'vue-i18n';
import { useAppStore } from '@/stores';
import { getBasemapSources, getStyleFromBasemapSource } from '@/utils/geometry/basemap';
const MARKER_ICON_URL =
'https://cdn.jsdelivr.net/gh/google/material-design-icons/png/maps/place/materialicons/24dp/1x/baseline_place_black_24dp.png';
export default defineComponent({
props: {
type: {
type: String as PropType<'geometry' | 'json' | 'csv' | 'string' | 'text'>,
default: null,
},
fieldData: {
type: Object as PropType<Field | undefined>,
default: undefined,
},
value: {
type: [Object, Array, String] as PropType<any>,
default: null,
},
loading: {
type: Boolean,
default: true,
},
disabled: {
type: Boolean,
default: false,
},
geometryFormat: {
type: String as PropType<GeometryFormat>,
default: undefined,
},
geometryType: {
type: String as PropType<GeometryType>,
default: undefined,
},
defaultView: {
type: Object,
default: () => ({}),
},
},
emits: ['input'],
setup(props, { emit }) {
const { t } = useI18n();
const container = ref<HTMLElement | null>(null);
let map: Map;
let mapLoading = ref(true);
let currentGeometry: Geometry | null | undefined;
const geometryOptionsError = ref<string | null>();
const geometryParsingError = ref<string | TranslateResult>();
const geometryType = (props.fieldData?.schema?.geometry_type ?? props.geometryType) as GeometryType;
const geometryFormat = props.geometryFormat || getGeometryFormatForType(props.type)!;
const basemaps = getBasemapSources();
const appStore = useAppStore();
const { basemap } = toRefs(appStore);
const style = computed(() => {
const source = basemaps.find((source) => source.name == basemap.value) ?? basemaps[0];
return basemap.value, getStyleFromBasemapSource(source);
});
let parse: GeoJSONParser;
let serialize: GeoJSONSerializer;
try {
parse = getParser({ geometryFormat, geometryField: 'value' });
serialize = getSerializer({ geometryFormat, geometryField: 'value' });
} catch (error) {
geometryOptionsError.value = error;
}
const mapboxKey = getSetting('mapbox_key');
const controls = {
draw: new MapboxDraw(getDrawOptions(geometryType)),
fitData: new ButtonControl('mapboxgl-ctrl-fitdata', fitDataBounds),
navigation: new NavigationControl(),
geolocate: new GeolocateControl(),
};
onMounted(() => {
setupMap();
});
onUnmounted(() => {
map.remove();
});
return {
t,
container,
mapLoading,
resetValue,
geometryParsingError,
geometryOptionsError,
basemaps,
basemap,
};
function setupMap() {
map = new Map({
container: container.value!,
style: style.value,
attributionControl: false,
...props.defaultView,
...(mapboxKey ? { accessToken: mapboxKey } : {}),
});
map.addControl(controls.navigation, 'top-left');
map.addControl(controls.geolocate, 'top-left');
map.addControl(controls.fitData, 'top-left');
map.addControl(controls.draw as IControl, 'top-left');
if (mapboxKey) {
map.addControl(new MapboxGeocoder({ accessToken: mapboxKey, marker: false }), 'top-right');
}
map.on('load', async () => {
map.resize();
mapLoading.value = false;
await addMarkerImage();
map.on('basemapselect', () => {
map.once('styledata', async () => {
await addMarkerImage();
});
});
map.on('draw.create', handleDrawUpdate);
map.on('draw.delete', handleDrawUpdate);
map.on('draw.update', handleDrawUpdate);
map.on('draw.modechange', handleDrawModeChange);
});
watch(
() => props.value,
(value) => {
if (!value) {
controls.draw.deleteAll();
currentGeometry = null;
if (geometryType) {
const snaked = snakeCase(geometryType.replace('Multi', ''));
const mode = `draw_${snaked}` as any;
controls.draw.changeMode(mode);
}
} else {
if (!isEqual(value, currentGeometry && serialize(currentGeometry))) {
loadValueFromProps();
}
}
if (props.disabled) {
controls.draw.changeMode('static');
}
},
{ immediate: true }
);
watch(
() => style.value,
async () => {
map.removeControl(controls.draw);
map.setStyle(style.value, { diff: false });
controls.draw = new MapboxDraw(getDrawOptions(geometryType));
await addMarkerImage();
map.addControl(controls.draw as IControl, 'top-left');
loadValueFromProps();
}
);
}
function resetValue(hard: boolean) {
geometryParsingError.value = undefined;
if (hard) emit('input', null);
}
function addMarkerImage() {
return new Promise((resolve, reject) => {
map.loadImage(MARKER_ICON_URL, (error: any, image: any) => {
if (error) reject(error);
map.addImage('place', image, { sdf: true });
resolve(true);
});
});
}
function fitDataBounds(options: CameraOptions & AnimationOptions) {
if (map && currentGeometry) {
map.fitBounds(currentGeometry.bbox! as LngLatBoundsLike, {
padding: 80,
maxZoom: 8,
...options,
});
}
}
function getDrawOptions(type: GeometryType): any {
const options = {
styles,
controls: {},
userProperties: true,
displayControlsDefault: false,
modes: Object.assign(MapboxDraw.modes, {
static: StaticMode,
}),
} as any;
if (props.disabled) {
return options;
}
if (!type) {
options.controls.line_string = true;
options.controls.polygon = true;
options.controls.point = true;
options.controls.trash = true;
return options;
} else {
const base = snakeCase(type!.replace('Multi', ''));
options.controls[base] = true;
options.controls.trash = true;
return options;
}
}
function isTypeCompatible(a?: GeometryType, b?: GeometryType): boolean {
if (!a || !b) {
return true;
}
if (a.startsWith('Multi')) {
return a.replace('Multi', '') == b.replace('Multi', '');
}
return a == b;
}
function loadValueFromProps() {
try {
controls.draw.deleteAll();
const initialValue = parse(props);
if (!props.disabled && !isTypeCompatible(geometryType, initialValue!.type)) {
geometryParsingError.value = t('interfaces.map.unexpected_geometry', {
expected: geometryType,
got: initialValue!.type,
});
}
const flattened = flatten(initialValue);
for (const geometry of flattened) {
controls.draw.add(geometry);
}
currentGeometry = getCurrentGeometry();
currentGeometry!.bbox = getBBox(currentGeometry!);
if (geometryParsingError.value) {
const bbox = getBBox(initialValue!) as LngLatBoundsLike;
map.fitBounds(bbox, { padding: 0, maxZoom: 8, duration: 0 });
} else {
fitDataBounds({ duration: 0 });
}
} catch (error) {
geometryParsingError.value = error;
}
}
function getCurrentGeometry(): Geometry | null {
const features = controls.draw.getAll().features;
const geometries = features.map((f) => f.geometry) as (SimpleGeometry | MultiGeometry)[];
let result: Geometry;
if (geometries.length == 0) {
return null;
} else if (!geometryType) {
if (geometries.length > 1) {
result = { type: 'GeometryCollection', geometries };
} else {
result = geometries[0];
}
} else if (geometryType.startsWith('Multi')) {
const coordinates = geometries
.filter(({ type }) => `Multi${type}` == geometryType)
.map(({ coordinates }) => coordinates);
result = { type: geometryType, coordinates } as Geometry;
} else {
result = geometries[geometries.length - 1];
}
result!.bbox = getBBox(result!);
return result;
}
function handleDrawModeChange(event: any) {
if (!props.disabled && event.mode.startsWith('draw') && geometryType && !geometryType.startsWith('Multi')) {
for (const feature of controls.draw.getAll().features.slice(0, -1)) {
controls.draw.delete(feature.id as string);
}
}
}
function handleDrawUpdate() {
currentGeometry = getCurrentGeometry();
if (!currentGeometry) {
controls.draw.deleteAll();
emit('input', null);
} else {
emit('input', serialize(currentGeometry));
}
}
},
});
</script>
<style lang="scss">
.mapbox-gl-draw_point::after {
content: 'add_location';
}
.mapbox-gl-draw_line::after {
content: 'timeline';
}
.mapbox-gl-draw_polygon::after {
content: 'category';
}
.mapbox-gl-draw_trash::after {
content: 'delete';
}
.mapbox-gl-draw_uncombine::after {
content: 'call_split';
}
.mapbox-gl-draw_combine::after {
content: 'call_merge';
}
</style>
<style lang="scss" scoped>
.interface-map {
overflow: hidden;
border: var(--border-width) solid var(--border-normal);
border-radius: var(--border-radius);
.map {
position: relative;
width: 100%;
height: 500px;
&.error,
&.loading {
opacity: 0.25;
}
}
.v-info {
padding: 20px;
background-color: var(--background-subdued);
border-radius: var(--border-radius);
box-shadow: var(--card-shadow);
}
.basemap-select {
position: absolute;
bottom: 10px;
left: 10px;
}
}
.center {
position: absolute;
top: 50%;
left: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
.v-button.hard-reset {
--v-button-background-color: var(--danger-10);
--v-button-color: var(--danger);
--v-button-background-color-hover: var(--danger-25);
--v-button-color-hover: var(--danger);
}
</style>

View File

@@ -0,0 +1,132 @@
<template>
<div class="form-grid">
<div class="field half-left">
<div class="type-label">{{ t('interfaces.map.geometry_format') }}</div>
<v-input v-model="geometryFormat" :disabled="true" :value="t(`interfaces.map.${compatibleFormat}`)" />
</div>
<div class="field half-right">
<div class="type-label">{{ t('interfaces.map.geometry_type') }}</div>
<v-select
v-model="geometryType"
:placeholder="t('any')"
:show-deselect="true"
:disabled="!!nativeGeometryType || geometryFormat == 'lnglat'"
:items="GEOMETRY_TYPES.map((value) => ({ value, text: value }))"
/>
</div>
<div class="field">
<div class="type-label">{{ t('interfaces.map.default_view') }}</div>
<div ref="mapContainer" class="map"></div>
</div>
</div>
</template>
<script lang="ts">
import { useI18n } from 'vue-i18n';
import { ref, defineComponent, PropType, watch, onMounted, onUnmounted, computed, toRefs } from 'vue';
import { GEOMETRY_TYPES } from '@directus/shared/constants';
import { Field, GeometryType, GeometryFormat, GeometryOptions } from '@directus/shared/types';
import { getGeometryFormatForType } from '@/utils/geometry';
import { getBasemapSources, getStyleFromBasemapSource } from '@/utils/geometry/basemap';
import 'maplibre-gl/dist/maplibre-gl.css';
import { Map, CameraOptions } from 'maplibre-gl';
import { useAppStore } from '@/stores';
export default defineComponent({
props: {
collection: {
type: String,
required: true,
},
fieldData: {
type: Object as PropType<Field>,
default: null,
},
value: {
type: Object as PropType<GeometryOptions & { defaultView?: CameraOptions }>,
default: null,
},
},
emits: ['input'],
setup(props, { emit }) {
const { t } = useI18n();
const isGeometry = props.fieldData.type == 'geometry';
const nativeGeometryType = isGeometry ? (props.fieldData!.schema!.geometry_type as GeometryType) : undefined;
const compatibleFormat = isGeometry ? ('native' as const) : getGeometryFormatForType(props.fieldData.type);
const geometryFormat = ref<GeometryFormat>(compatibleFormat!);
const geometryType = ref<GeometryType>(
geometryFormat.value == 'lnglat' ? 'Point' : nativeGeometryType ?? props.value?.geometryType
);
const defaultView = ref<CameraOptions | undefined>(props.value?.defaultView);
watch(
[geometryFormat, geometryType, defaultView],
() => {
const type = geometryFormat.value == 'lnglat' ? 'Point' : geometryType;
emit('input', { defaultView, geometryFormat, geometryType: type });
},
{ immediate: true }
);
const mapContainer = ref<HTMLElement | null>(null);
let map: Map;
const basemaps = getBasemapSources();
const appStore = useAppStore();
const { basemap } = toRefs(appStore);
const style = computed(() => {
const source = basemaps.find((source) => source.name == basemap.value) ?? basemaps[0];
return getStyleFromBasemapSource(source);
});
onMounted(() => {
map = new Map({
container: mapContainer.value!,
style: style.value,
attributionControl: false,
...(defaultView.value || {}),
});
map.on('moveend', () => {
defaultView.value = {
center: map.getCenter(),
zoom: map.getZoom(),
bearing: map.getBearing(),
pitch: map.getPitch(),
};
});
});
onUnmounted(() => {
map.remove();
});
return {
t,
isGeometry,
nativeGeometryType,
compatibleFormat,
geometryFormat,
GEOMETRY_TYPES,
geometryType,
mapContainer,
fitBounds,
};
},
});
</script>
<style lang="scss" scoped>
@import '@/styles/mixins/form-grid';
.form-grid {
@include form-grid;
}
.map {
height: 400px;
overflow: hidden;
border: var(--border-width) solid var(--border-normal);
border-radius: var(--border-radius);
}
</style>

View File

@@ -0,0 +1,219 @@
export default [
{
id: 'directus-polygon-fill-inactive',
type: 'fill',
filter: ['all', ['==', 'active', 'false'], ['==', '$type', 'Polygon'], ['!=', 'mode', 'static']],
paint: {
'fill-color': '#3bb2d0',
'fill-outline-color': '#3bb2d0',
'fill-opacity': 0.1,
},
},
{
id: 'directus-polygon-fill-active',
type: 'fill',
filter: ['all', ['==', 'active', 'true'], ['==', '$type', 'Polygon']],
paint: {
'fill-color': '#fbb03b',
'fill-outline-color': '#fbb03b',
'fill-opacity': 0.1,
},
},
{
id: 'directus-polygon-midpoint',
type: 'circle',
filter: ['all', ['==', '$type', 'Point'], ['==', 'meta', 'midpoint']],
paint: {
'circle-radius': 3,
'circle-color': '#fbb03b',
},
},
{
id: 'directus-polygon-stroke-inactive',
type: 'line',
filter: ['all', ['==', 'active', 'false'], ['==', '$type', 'Polygon'], ['!=', 'mode', 'static']],
layout: {
'line-cap': 'round',
'line-join': 'round',
},
paint: {
'line-color': '#3bb2d0',
'line-width': 2,
},
},
{
id: 'directus-polygon-stroke-active',
type: 'line',
filter: ['all', ['==', 'active', 'true'], ['==', '$type', 'Polygon']],
layout: {
'line-cap': 'round',
'line-join': 'round',
},
paint: {
'line-color': '#fbb03b',
'line-dasharray': [0.2, 2],
'line-width': 2,
},
},
{
id: 'directus-line-inactive',
type: 'line',
filter: ['all', ['==', 'active', 'false'], ['==', '$type', 'LineString'], ['!=', 'mode', 'static']],
layout: {
'line-cap': 'round',
'line-join': 'round',
},
paint: {
'line-color': '#3bb2d0',
'line-width': 2,
},
},
{
id: 'directus-line-active',
type: 'line',
filter: ['all', ['==', '$type', 'LineString'], ['==', 'active', 'true']],
layout: {
'line-cap': 'round',
'line-join': 'round',
},
paint: {
'line-color': '#fbb03b',
'line-dasharray': [0.2, 2],
'line-width': 2,
},
},
{
id: 'directus-polygon-and-line-vertex-stroke-inactive',
type: 'circle',
filter: ['all', ['==', 'meta', 'vertex'], ['==', '$type', 'Point'], ['!=', 'mode', 'static']],
paint: {
'circle-radius': 5,
'circle-color': '#fff',
},
},
{
id: 'directus-polygon-and-line-vertex-inactive',
type: 'circle',
filter: ['all', ['==', 'meta', 'vertex'], ['==', '$type', 'Point'], ['!=', 'mode', 'static']],
paint: {
'circle-radius': 3,
'circle-color': '#fbb03b',
},
},
{
id: 'directus-points-shadow',
filter: [
'all',
['==', 'active', 'false'],
['==', '$type', 'Point'],
['==', 'meta', 'feature'],
['!=', 'meta', 'midpoint'],
],
type: 'circle',
paint: {
'circle-pitch-alignment': 'map',
'circle-blur': 1,
'circle-opacity': 0.5,
'circle-radius': 6,
},
},
{
id: 'directus-point-inactive',
filter: [
'all',
['==', '$type', 'Point'],
['==', 'active', 'false'],
['==', 'meta', 'feature'],
['!=', 'meta', 'midpoint'],
],
type: 'symbol',
layout: {
'icon-image': 'place',
'icon-anchor': 'bottom',
'icon-allow-overlap': true,
'icon-size': 2,
'icon-offset': [0, 3],
},
paint: {
'icon-color': '#3bb2d0',
},
},
{
id: 'directus-point-active',
filter: [
'all',
['==', '$type', 'Point'],
['==', 'active', 'true'],
['==', 'meta', 'feature'],
['!=', 'meta', 'midpoint'],
],
type: 'symbol',
layout: {
'icon-image': 'place',
'icon-anchor': 'bottom',
'icon-allow-overlap': true,
'icon-size': 2,
'icon-offset': [0, 3],
},
paint: {
'icon-color': '#fbb03b',
},
},
{
id: 'directus-point-static',
type: 'symbol',
filter: [
'all',
['==', '$type', 'Point'],
['==', 'mode', 'static'],
['==', 'meta', 'feature'],
['!=', 'meta', 'midpoint'],
],
layout: {
'icon-image': 'place',
'icon-anchor': 'bottom',
'icon-allow-overlap': true,
'icon-size': 2,
'icon-offset': [0, 3],
},
paint: {
'icon-color': '#404040',
},
},
{
id: 'directus-polygon-fill-static',
type: 'fill',
filter: ['all', ['==', 'mode', 'static'], ['==', '$type', 'Polygon']],
paint: {
'fill-color': '#404040',
'fill-outline-color': '#404040',
'fill-opacity': 0.1,
},
},
{
id: 'directus-polygon-stroke-static',
type: 'line',
filter: ['all', ['==', 'mode', 'static'], ['==', '$type', 'Polygon']],
layout: {
'line-cap': 'round',
'line-join': 'round',
},
paint: {
'line-color': '#404040',
'line-width': 2,
},
},
{
id: 'directus-line-static',
type: 'line',
filter: ['all', ['==', 'mode', 'static'], ['==', '$type', 'LineString']],
layout: {
'line-cap': 'round',
'line-join': 'round',
},
paint: {
'line-color': '#404040',
'line-width': 2,
},
},
];