diff --git a/docs/docs/index.md b/docs/docs/index.md
index f42f55a..1868031 100644
--- a/docs/docs/index.md
+++ b/docs/docs/index.md
@@ -37,7 +37,8 @@ title: API
| mapControl | 控件显隐 | [MapControlProps](#mapcontrolprops) | `-` |
| toolbar | 头部组件显隐 | [ToolbarProps](#toolbarprops) | `-` |
| tabItems | 侧面版标签页选项卡内容 | [TabItemType](https://ant-design.antgroup.com/components/tabs-cn#tabitemtype) | `-` |
-| showIndex | 是否展示元素序号 | `boolean` | `false` |
+| showTextLayer | 是否展示元素文本 | `boolean` | `false` |
+| textLayerFields | 展示元素文本的字段,不选则展示元素序号 | `string[] | undefined` | `undefined` |
| wasmPath | sam 组件的 wasm 路径 | `string` | `\` |
#### `tabItems`
@@ -152,6 +153,7 @@ LngLat 文本编辑器,可以通过输入 LngLat 数据实现数据展示(目
| administrativeSelectControl | 行政区域选择控件 |
| mapAdministrativeControl | 查看当前行政区域控件 |
| logoControl | Logo 控件 |
+| textLayerControl | 文本图层 控件 |
#### toolbar
diff --git a/package.json b/package.json
index 6669d0d..4ad586a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@antv/l7-editor",
- "version": "1.1.8",
+ "version": "1.1.9",
"description": "Geographic data editing tool based on L7",
"files": [
"lib",
diff --git a/src/components/app-header/btn/setting-btn.tsx b/src/components/app-header/btn/setting-btn.tsx
index 1567ae5..d7ba737 100644
--- a/src/components/app-header/btn/setting-btn.tsx
+++ b/src/components/app-header/btn/setting-btn.tsx
@@ -6,14 +6,8 @@ import { useGlobal } from '../../../recoil';
export const SettingBtn = () => {
const [isModalOpen, setIsModalOpen] = useState(false);
- const {
- popupTrigger,
- setPopupTrigger,
- autoFitBounds,
- setAutoFitBounds,
- showIndex,
- setShowIndex
- } = useGlobal();
+ const { popupTrigger, setPopupTrigger, autoFitBounds, setAutoFitBounds } =
+ useGlobal();
const { t } = useTranslation();
const [form] = Form.useForm();
@@ -55,14 +49,12 @@ export const SettingBtn = () => {
initialValues={{
popupTrigger,
autoFitBounds,
- showIndex,
}}
style={{ textAlign: 'right' }}
onFinish={(e) => {
setIsModalOpen(false);
setPopupTrigger(e.popupTrigger);
setAutoFitBounds(e.autoFitBounds);
- setShowIndex(e.showIndex)
}}
>
{
>
-
-
-
- {t('btn.setting_btn.kaiQi')}
-
-
- {t('btn.setting_btn.guanBi')}
-
-
-
>
diff --git a/src/components/app-header/index.tsx b/src/components/app-header/index.tsx
index 2b705a8..638e5f8 100644
--- a/src/components/app-header/index.tsx
+++ b/src/components/app-header/index.tsx
@@ -209,6 +209,11 @@ export const AppHeader: React.FC = ({ toolbar }) => {
description: t('app_header.constants.keXuanZeBuTong'),
target: () => document.getElementById('l7-editor-aMap')!,
},
+ {
+ title: t('text-layer-control_wenBenTuCengPeiZhi'),
+ description: t('text-layer-control_description'),
+ target: () => document.getElementById('text-layer-control')!,
+ },
{
title: t('app_header.constants.gEOJS'),
description: t('app_header.constants.keYiTongGuoBian'),
diff --git a/src/components/layer-list/index.tsx b/src/components/layer-list/index.tsx
index d3a17cd..9c55bbe 100644
--- a/src/components/layer-list/index.tsx
+++ b/src/components/layer-list/index.tsx
@@ -15,11 +15,12 @@ import { FeatureKey, LayerId, LayerZIndex } from '../../constants';
import { useFilterFeatures } from '../../hooks';
import { useFeature, useGlobal } from '../../recoil';
import { getPointImage } from '../../utils/change-image-color';
+import { EditorTextLayer } from '../text-layer';
export const LayerList: React.FC = () => {
const scene = useScene();
const [isMounted, setIsMounted] = useState(false);
- const { layerColor, coordConvert, baseMap } = useGlobal();
+ const { layerColor, coordConvert, baseMap, showTextLayer } = useGlobal();
const { transformCoord } = useFeature();
const { features: newFeatures } = useFilterFeatures();
const [features, setFeatures] = useState([]);
@@ -111,6 +112,7 @@ export const LayerList: React.FC = () => {
state={{ active: { color: activeColor } }}
zIndex={LayerZIndex}
/>
+ {showTextLayer && }
>
) : null;
};
diff --git a/src/components/map-control-group/index.tsx b/src/components/map-control-group/index.tsx
index f4abd5b..6d36da8 100644
--- a/src/components/map-control-group/index.tsx
+++ b/src/components/map-control-group/index.tsx
@@ -22,6 +22,7 @@ import MapThemeControl from './map-theme-control';
import { OfficialLayerControl } from './official-layer-control';
import { SamControl } from './sam-control';
import useStyles from './styles';
+import { TextLayerControl } from './text-layer-control';
type MapControlGroupProps = {
mapControl?: MapControlProps;
@@ -43,6 +44,7 @@ const DefaultMapControl: MapControlProps = {
administrativeSelectControl: true,
mapAdministrativeControl: true,
logoControl: true,
+ textLayerControl: true,
};
export const MapControlGroup: React.FC = ({
mapControl,
@@ -70,7 +72,7 @@ export const MapControlGroup: React.FC = ({
{isControlGroupState.drawControl && }
{isControlGroupState.clearControl && }
{isControlGroupState.zoomControl && (
-
+
)}
{isControlGroupState.mapAdministrativeControl && (
@@ -96,6 +98,7 @@ export const MapControlGroup: React.FC = ({
/>
)}
{layerType.includes(OfficeLayerEnum.GoogleSatellite) && }
+ {isControlGroupState.textLayerControl && }
>
);
};
diff --git a/src/components/map-control-group/official-layer-control/index.tsx b/src/components/map-control-group/official-layer-control/index.tsx
index 04afa7a..a2db7ad 100644
--- a/src/components/map-control-group/official-layer-control/index.tsx
+++ b/src/components/map-control-group/official-layer-control/index.tsx
@@ -242,7 +242,7 @@ export function OfficialLayerControl() {
return (
<>
-
+
{
diff --git a/src/components/map-control-group/official-layer-control/styles.ts b/src/components/map-control-group/official-layer-control/styles.ts
index 5286b8f..2c77d74 100644
--- a/src/components/map-control-group/official-layer-control/styles.ts
+++ b/src/components/map-control-group/official-layer-control/styles.ts
@@ -26,7 +26,7 @@ const useStyle = () => {
`,
hideOfficeLayerBtn: css`
height: 127px;
- width: 20px;
+ width: 28px;
display: flex;
align-items: center;
justify-content: center;
diff --git a/src/components/map-control-group/text-layer-control/index.tsx b/src/components/map-control-group/text-layer-control/index.tsx
new file mode 100644
index 0000000..590fca1
--- /dev/null
+++ b/src/components/map-control-group/text-layer-control/index.tsx
@@ -0,0 +1,80 @@
+import { CustomControl } from '@antv/larkmap';
+import { Form, Popover, Select, Switch, Tooltip } from 'antd';
+import React, { useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useFeature, useGlobal } from '../../../recoil';
+import { IconFont } from '../../iconfont';
+import useStyles from '../styles';
+import useStyle from './style';
+
+export type TextLayerControlProps = {};
+
+export const TextLayerControl: React.FC = () => {
+ const styles = useStyles();
+ const style = useStyle();
+ const {
+ showTextLayer,
+ setShowTextLayer,
+ textLayerFields,
+ setTextLayerFields,
+ } = useGlobal();
+ const { features } = useFeature();
+ const [fields, setFields] = useState
([]);
+ const { t } = useTranslation();
+
+ const refreshFields = () => {
+ const newFieldSet = new Set();
+ features.forEach((feature) => {
+ const properties = feature.properties;
+ if (properties) {
+ Object.keys(properties).forEach((key) => {
+ newFieldSet.add(key);
+ });
+ }
+ });
+ setFields(Array.from(newFieldSet));
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+ }
+ trigger="click"
+ onOpenChange={(visible) => {
+ if (visible) {
+ refreshFields();
+ }
+ }}
+ >
+
+
+
+
+
+ );
+};
diff --git a/src/components/map-control-group/text-layer-control/style.ts b/src/components/map-control-group/text-layer-control/style.ts
new file mode 100644
index 0000000..5c21cb4
--- /dev/null
+++ b/src/components/map-control-group/text-layer-control/style.ts
@@ -0,0 +1,12 @@
+import { css } from '@emotion/css';
+
+const useStyle = () => {
+ return {
+ textLayerForm: css`
+ .ant-form-item {
+ margin-bottom: 12px !important;
+ }
+ `,
+ };
+};
+export default useStyle;
diff --git a/src/components/text-layer/index.tsx b/src/components/text-layer/index.tsx
index 7d77fd6..7368846 100644
--- a/src/components/text-layer/index.tsx
+++ b/src/components/text-layer/index.tsx
@@ -1,60 +1,92 @@
import type { TextLayerProps } from '@antv/larkmap';
import { TextLayer } from '@antv/larkmap';
+import type { Feature, LineString } from '@turf/turf';
import { center } from '@turf/turf';
+import { cloneDeep } from 'lodash-es';
import React, { useMemo } from 'react';
import { FeatureKey } from '../../constants';
import { useFilterFeatures } from '../../hooks';
import { useFeature, useGlobal } from '../../recoil';
+import { centerOfLine } from '../../utils';
export const EditorTextLayer = () => {
const { transformCoord } = useFeature();
const { features: newFeatures } = useFilterFeatures();
- const { layerColor } = useGlobal();
+ const { layerColor, textLayerFields } = useGlobal();
- const layerOptions: Omit = useMemo(() => {
+ const textOptions: Omit = useMemo(() => {
return {
zIndex: 101,
- field: 'name',
+ field: 'text',
style: {
fill: `${layerColor}`,
opacity: 1,
- fontSize: 18,
+ fontSize: 16,
stroke: '#fff',
strokeWidth: 2,
textAllowOverlap: true,
padding: [10, 10] as [number, number],
- textOffset: [0, -18],
},
};
}, [layerColor]);
+ const pointTextOptions: Omit = useMemo(() => {
+ const newLayerOptions = cloneDeep(textOptions);
+ newLayerOptions.style!.textOffset = [0, -20];
+ return newLayerOptions;
+ }, [textOptions]);
+
const sourceData = useMemo(() => {
- const transformData = transformCoord(newFeatures).map((item) => {
- return {
- data: center(item),
- //@ts-ignore
- featureIndex: item.properties?.[FeatureKey.Index],
- };
- });
- const data = transformData.map((item) => {
+ const data = transformCoord(newFeatures).map((item) => {
+ // @ts-ignore
+ const featureIndex = item.properties?.[FeatureKey.Index];
+ const [x, y] = (() => {
+ if (item.geometry.type === 'LineString') {
+ return centerOfLine(item as Feature).geometry.coordinates;
+ }
+ if (item.geometry.type === 'Point') {
+ return item.geometry.coordinates;
+ } else {
+ return center(item).geometry.coordinates;
+ }
+ })();
+
+ let text = `${featureIndex + 1}`;
+
+ if (textLayerFields?.length) {
+ text = textLayerFields
+ .map((field) => {
+ return String(item.properties?.[field] ?? '');
+ })
+ .join(' ');
+ }
+
return {
- //@ts-ignore
- x: item.data.geometry.coordinates[0],
- //@ts-ignore
- y: item.data.geometry.coordinates[1],
- name: `${item.featureIndex + 1}`,
+ x,
+ y,
+ text,
+ type: item.geometry.type,
};
});
return data;
- }, [transformCoord, newFeatures]);
-
+ }, [transformCoord, newFeatures, textLayerFields]);
+
return (
-
+ <>
+ item.type !== 'Point'),
+ parser: { type: 'json', x: 'x', y: 'y' },
+ }}
+ />
+ item.type === 'Point'),
+ parser: { type: 'json', x: 'x', y: 'y' },
+ }}
+ />
+ >
);
};
diff --git a/src/constants/iconfont.js b/src/constants/iconfont.js
index e933f51..b14a5dc 100644
--- a/src/constants/iconfont.js
+++ b/src/constants/iconfont.js
@@ -1 +1 @@
-window._iconfont_svg_string_3567033='',function(t){var c=(c=document.getElementsByTagName("script"))[c.length-1],h=c.getAttribute("data-injectcss"),c=c.getAttribute("data-disable-injectsvg");if(!c){var a,l,i,e,o,n=function(c,h){h.parentNode.insertBefore(c,h);};if(h&&!t.__iconfont__svg__cssinject__){t.__iconfont__svg__cssinject__=!0;try{document.write("");}catch(c){console&&console.log(c);}}a=function(){var c,h=document.createElement("div");h.innerHTML=t._iconfont_svg_string_3567033,(h=h.getElementsByTagName("svg")[0])&&(h.setAttribute("aria-hidden","true"),h.style.position="absolute",h.style.width=0,h.style.height=0,h.style.overflow="hidden",h=h,(c=document.body).firstChild?n(h,c.firstChild):c.appendChild(h));},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(a,0):(l=function(){document.removeEventListener("DOMContentLoaded",l,!1),a();},document.addEventListener("DOMContentLoaded",l,!1)):document.attachEvent&&(i=a,e=t.document,o=!1,s(),e.onreadystatechange=function(){"complete"==e.readyState&&(e.onreadystatechange=null,v());});}function v(){o||(o=!0,i());}function s(){try{e.documentElement.doScroll("left");}catch(c){return void setTimeout(s,50);}v();}}(window);
\ No newline at end of file
+window._iconfont_svg_string_3567033='',function(t){var c=(c=document.getElementsByTagName("script"))[c.length-1],h=c.getAttribute("data-injectcss"),c=c.getAttribute("data-disable-injectsvg");if(!c){var a,l,i,e,o,n=function(c,h){h.parentNode.insertBefore(c,h)};if(h&&!t.__iconfont__svg__cssinject__){t.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(c){console&&console.log(c)}}a=function(){var c,h=document.createElement("div");h.innerHTML=t._iconfont_svg_string_3567033,(h=h.getElementsByTagName("svg")[0])&&(h.setAttribute("aria-hidden","true"),h.style.position="absolute",h.style.width=0,h.style.height=0,h.style.overflow="hidden",h=h,(c=document.body).firstChild?n(h,c.firstChild):c.appendChild(h))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(a,0):(l=function(){document.removeEventListener("DOMContentLoaded",l,!1),a()},document.addEventListener("DOMContentLoaded",l,!1)):document.attachEvent&&(i=a,e=t.document,o=!1,s(),e.onreadystatechange=function(){"complete"==e.readyState&&(e.onreadystatechange=null,v())})}function v(){o||(o=!0,i())}function s(){try{e.documentElement.doScroll("left")}catch(c){return void setTimeout(s,50)}v()}}(window);
\ No newline at end of file
diff --git a/src/constants/index.ts b/src/constants/index.ts
index ea45793..c96c5cb 100644
--- a/src/constants/index.ts
+++ b/src/constants/index.ts
@@ -1,8 +1,5 @@
export const RightPanelWidthRange = [20, 80];
-
-
-
export enum LocalStorageKey {
RightPanelWidth = 'RightPanelWidth',
MapOptions = 'MapOptions',
@@ -18,11 +15,12 @@ export enum LocalStorageKey {
Convert = 'Convert',
theme = 'theme',
cityHistory = 'cityHistory',
- showIndex = 'showIndex',
+ showTextLayer = 'showTextLayer',
+ textLayerFields = 'textLayerFields',
locale = 'locale',
firstOpening = 'firstOpening',
- wasmPath = "wasmPath",
- customTiles = "customTiles"
+ wasmPath = 'wasmPath',
+ customTiles = 'customTiles',
}
export enum LayerId {
diff --git a/src/locales/langs/en-US.ts b/src/locales/langs/en-US.ts
index 9f2aeb9..848dcb6 100644
--- a/src/locales/langs/en-US.ts
+++ b/src/locales/langs/en-US.ts
@@ -223,5 +223,12 @@ export default {
'official_layer_control.index.tinJiaWaPian': 'Add Tile Layer Address',
'official_layer_control.index.kongGe': 'Input cannot be a space!',
'official_layer_control.index.shangchuan': 'Please upload pictures',
-
+ 'text-layer-control_wenBenBiaoZhu': 'Text callout layer configuration',
+ 'text-layer-control_shiFouZhanShiTuCeng': 'Show layer',
+ 'text-layer-control_zhanShiZiDuan': 'Show Fields',
+ 'text-layer-control_buXuan':
+ 'If not selected, the element serial number will be displayed by default.',
+ 'text-layer-control_wenBenTuCengPeiZhi': 'Text Layer Configuration',
+ 'text-layer-control_description':
+ 'Open the text layer configuration, you can display the serial number of all the current data, you can also use the multi-select box to properties fields in the data to select different data for display.',
};
diff --git a/src/locales/langs/zh-CN.ts b/src/locales/langs/zh-CN.ts
index be8f79b..ef8a774 100644
--- a/src/locales/langs/zh-CN.ts
+++ b/src/locales/langs/zh-CN.ts
@@ -173,7 +173,7 @@ export default {
'map_control_group.sam.zhiNengXuanZe': '地块智能识别',
'map_control_group.sam.ziDongShiBie': '地块识别区域边界',
'map_control_group.sam.zhiNengShiBieGuanBi': '地块智能识别功能已关闭',
- 'map_control_group.sam.diKuaiShiBieShiBei':'地块智能识别模型加载失败',
+ 'map_control_group.sam.diKuaiShiBieShiBei': '地块智能识别模型加载失败',
'map_contorl_group.draw.draw': '单击开始绘制',
'map_contorl_group.draw.drawContinue': '单击继续绘制',
'map_contorl_group.draw.drawFinish': '单击继续绘制,双击结束绘制',
@@ -191,4 +191,10 @@ export default {
'official_layer_control.index.tinJiaWaPian': '添加瓦片图层地址',
'official_layer_control.index.kongGe': '输入不能为空格!',
'official_layer_control.index.shangchuan': '请上传图片',
+ 'text-layer-control_wenBenBiaoZhu': '文本标注图层配置',
+ 'text-layer-control_shiFouZhanShiTuCeng': '是否展示图层',
+ 'text-layer-control_zhanShiZiDuan': '展示字段',
+ 'text-layer-control_buXuan': '不选则默认展示元素序号',
+ 'text-layer-control_wenBenTuCengPeiZhi':'文本图层配置',
+ 'text-layer-control_description':'开启文本图层配置,可以显示当前所有数据的序号,也可以通过多选框在数据中properties字段去选择不同的数据来进行展示'
};
diff --git a/src/pages/components/editor.tsx b/src/pages/components/editor.tsx
index 6f7ced9..d198fa8 100644
--- a/src/pages/components/editor.tsx
+++ b/src/pages/components/editor.tsx
@@ -14,7 +14,6 @@ import {
MapControlGroup,
ResizePanel,
} from '../../components';
-import { EditorTextLayer } from '../../components/text-layer';
import { LocalStorageKey } from '../../constants';
import { LangList } from '../../locales';
import { useFeature, useGlobal } from '../../recoil';
@@ -26,7 +25,7 @@ type EditorProps = L7EditorProps;
export const Editor: React.FC = (props) => {
const { onFeatureChange } = props;
const { i18n, t } = useTranslation();
- const { theme, mapOptions, setMapOptions, showIndex, locale } = useGlobal();
+ const { theme, mapOptions, setMapOptions, locale } = useGlobal();
const styles = useStyle();
const { saveEditorText, bboxAutoFit, scene } = useFeature();
@@ -99,7 +98,6 @@ export const Editor: React.FC = (props) => {
left={
- {showIndex && }
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
index 1de4630..c3b0e4d 100644
--- a/src/pages/index.tsx
+++ b/src/pages/index.tsx
@@ -18,6 +18,8 @@ import {
officialLayersState,
popupTriggerState,
rightWidthState,
+ showTextLayerState,
+ textLayerFieldsState,
themeState,
wasmPathState,
} from '../recoil/atomState';
@@ -47,6 +49,8 @@ export const L7Editor = (props: L7EditorProps) => {
set(convertState, props?.coordConvert ?? 'GCJ02');
set(localeState, props?.locale ?? 'zh-CN');
set(wasmPathState, props?.wasmPath ?? '/');
+ set(showTextLayerState, props?.showTextLayer ?? false);
+ set(textLayerFieldsState, props?.textLayerFields ?? undefined);
};
}, [props]);
diff --git a/src/recoil/atomState.ts b/src/recoil/atomState.ts
index 31c2707..efb4404 100644
--- a/src/recoil/atomState.ts
+++ b/src/recoil/atomState.ts
@@ -148,10 +148,16 @@ const cityHistoryState = atom<{ value: string; label: string }[]>({
effects: [localStorageEffect(LocalStorageKey.cityHistory)],
});
-const showIndexState = atom({
- key: 'showIndex',
+const showTextLayerState = atom({
+ key: 'showTextLayer',
default: true,
- effects: [localStorageEffect(LocalStorageKey.showIndex)],
+ effects: [localStorageEffect(LocalStorageKey.showTextLayer)],
+});
+
+const textLayerFieldsState = atom({
+ key: 'textLayerFields',
+ default: undefined,
+ effects: [localStorageEffect(LocalStorageKey.textLayerFields)],
});
const customTilesState = atom<
@@ -163,7 +169,7 @@ const customTilesState = atom<
layers: string[];
}[]
>({
- key: 'showIndex',
+ key: 'showTextLayer',
default: [],
effects: [localStorageEffect(LocalStorageKey.customTiles)],
});
@@ -201,7 +207,8 @@ export {
rightWidthState,
savedTextState,
sceneState,
- showIndexState,
+ showTextLayerState,
+ textLayerFieldsState,
themeState,
wasmPathState,
};
diff --git a/src/recoil/global.ts b/src/recoil/global.ts
index 5bb1391..fafe5bd 100644
--- a/src/recoil/global.ts
+++ b/src/recoil/global.ts
@@ -13,7 +13,8 @@ import {
officialLayersState,
popupTriggerState,
rightWidthState,
- showIndexState,
+ showTextLayerState,
+ textLayerFieldsState,
themeState,
wasmPathState,
} from './atomState';
@@ -43,7 +44,10 @@ export default function useGlobal() {
const [cityHistory, setCityHistory] = useRecoilState(cityHistoryState);
- const [showIndex, setShowIndex] = useRecoilState(showIndexState);
+ const [showTextLayer, setShowTextLayer] = useRecoilState(showTextLayerState);
+
+ const [textLayerFields, setTextLayerFields] =
+ useRecoilState(textLayerFieldsState);
const [customTiles, setCustomTiles] = useRecoilState(customTilesState);
@@ -76,8 +80,10 @@ export default function useGlobal() {
setTheme,
cityHistory,
setCityHistory,
- showIndex,
- setShowIndex,
+ showTextLayer,
+ setShowTextLayer,
+ textLayerFields,
+ setTextLayerFields,
customTiles,
setCustomTiles,
locale,
diff --git a/src/types/l7editor.ts b/src/types/l7editor.ts
index b28f4d6..dc24ed9 100644
--- a/src/types/l7editor.ts
+++ b/src/types/l7editor.ts
@@ -19,6 +19,7 @@ export interface MapControlProps {
fullscreenControl?: boolean;
administrativeSelectControl?: boolean;
mapAdministrativeControl?: boolean;
+ textLayerControl?: boolean;
}
export interface ToolbarProps {
@@ -116,10 +117,16 @@ export interface L7EditorProps {
*/
tabItems?: TabsProps['items'];
/**
- * 是否展示元素序号
+ * 是否展示元素文本
* @default false
*/
- showIndex?: boolean;
+ showTextLayer?: boolean;
+
+ /**
+ * 展示元素文本的字段,不选则展示元素序号
+ * @default undefined
+ */
+ textLayerFields?: string[];
/**
* 默认语言设置
*/
diff --git a/src/utils/feature.ts b/src/utils/feature.ts
new file mode 100644
index 0000000..584c5c5
--- /dev/null
+++ b/src/utils/feature.ts
@@ -0,0 +1,15 @@
+import type { Feature, LineString, Point } from '@turf/turf';
+import { length, lineSliceAlong, point } from '@turf/turf';
+import { last } from 'lodash-es';
+
+export const centerOfLine = (line: Feature): Feature => {
+ const lineLength = length(line, {
+ units: 'meters',
+ });
+ const position = last(
+ lineSliceAlong(line, 0, lineLength / 2, {
+ units: 'meters',
+ }).geometry.coordinates,
+ )!;
+ return point(position);
+};
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 65ae27c..77fe9d5 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -148,6 +148,7 @@ export const isRect = (feature: Feature) => {
return false;
};
+export * from './feature';
export * from './gcoord';
export * from './lnglat';
export * from './transform';