npm package: @grit-npm/grit-utils
Description: Collection of handy utils for use in JavaScript and OpenLayers projects.
Author: grit GmbH
Latest version: 0.3.8

# grit-utils

[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg?style=flat-square)](LICENSE)
[![ESM Ready](https://img.shields.io/badge/module-ESM%20only-orange?style=flat-square)](#)

> A collection of handy utility functions for JavaScript and OpenLayers projects.

---

## Features

- **Geometry helpers** for working with OpenLayers layers and features  
- **Styling utilities** to generate custom polygon styles  
- **Marker placement functions** for additional polygon highlighting  
- **General utilities** like a modern, Vuex-compatible `debounce` function  
- Fully **ESM-based** build with optimized Rollup output  
- **Tree-shakable** — only import what you need  

---

## Installation

```bash
npm install grit-utils
# or
yarn add grit-utils
# or
pnpm add grit-utils
```

## Usage

<details>
<summary><strong>geom</strong></summary> 

### Example: Add a feature to a layer

```js
import { addFeatureToLayer } from "@grit-npm/grit-utils";

// Assuming you have a Vector layer with a source:
addFeatureToLayer(myVectorLayer, myFeature);
```

### Example: Generate a polygon style
```js
import { generatePolygonStyle } from "@grit-npm/grit-utils";

const customStyle = generatePolygonStyle({
  stroke: { color: 'red', width: 2, lineDash: [4, 4] },
  fill:   { color: 'rgba(255,0,0,0.5)' }
})
```

### Example: Placing additional Polygon Marker
```js
import VectorLayer from "ol/layer/Vector";
import VectorSource from "ol/source/Vector";
import Feature from "ol/Feature";
import { Polygon } from "ol/geom";
import { placingAdditionalPolygonMarker } from "@grit-npm/grit-utils";
import mapMarker from "./mapMarker";
import store from "./store";

const vectorLayer = new VectorLayer({ source: new VectorSource() })
map.addLayer(vectorLayer)

const squareFeature = new Feature(new Polygon([[
  [0, 0],
  [100, 0],
  [100, 100],
  [0, 100],
  [0, 0]
]]))

placingAdditionalPolygonMarker(
  vectorLayer,
  mapMarker,
  store,
  "HighlightFkz",
  [squareFeature],
  defaultStyleObject,
  true
)
```

### Example: Placing additional Polygon Marker by Geometry
```js
import VectorLayer from "ol/layer/Vector";
import VectorSource from "ol/source/Vector";
import { Polygon } from "ol/geom";
import { placingAdditionalPolygonMarkerByGeom } from "@grit-npm/grit-utils";
import mapMarker from "./mapMarker";
import store from "./store";

const vectorLayer = new VectorLayer({ source: new VectorSource() })
map.addLayer(vectorLayer)

const square = new Polygon([[
  [0, 0],
  [100, 0],
  [100, 100],
  [0, 100],
  [0, 0]
]])

placingAdditionalPolygonMarkerByGeom(
  vectorLayer,
  mapMarker,
  store,
  "HighlightFkz",
  square,
  defaultStyleObject,
  false, // not clicked
  true   // use additional style
)
```

### Example: Remove Feature from Layer
```js
import VectorLayer from "ol/layer/Vector";
import VectorSource from "ol/source/Vector";
import Feature from "ol/Feature";
import { Polygon } from "ol/geom";
import { removeFeatureFromLayer } from "@grit-npm/grit-utils";

// 1. Create a vector layer and add it to your map
const layer = new VectorLayer({
  source: new VectorSource(),
});
map.addLayer(layer)

// 2. Create a simple polygon feature
const square = new Polygon([[
  [0, 0],
  [10, 0],
  [10, 10],
  [0, 10],
  [0, 0]
]])
const feature = new Feature(square);

// 3. Add the feature to the layer
layer.getSource().addFeature(feature);

// 4. Remove the feature from the layer
removeFeatureFromLayer(layer, feature);
```
</details>



<details>
<summary><strong>utils</strong></summary>

### Example: Change active module in menu
```js
import { changeModule } from "@grit-npm/grit-utils";
import store from "./store";

await changeModule(store, "measure", "mainMenu");
```

### Example: Debounce any function (supports Vuex dispatch)
```js
import { debounce } from "@grit-npm/grit-utils";

// Normal function debounce
const debouncedResize = debounce(onResize);

window.addEventListener("resize", debouncedResize);

// Vuex dispatch debounce
await debounce.dispatch(
  store,
  "Modules/Module/FETCH_URL",
  { id },
  { root: true },
  500,
);
```

### Example: Get module properties from store configuration
```js
import { getModuleProps } from "@grit-npm/grit-utils";
import store from "./store";

// Get configuration for a specific module
const moduleConfig = getModuleProps(store, "measure");
// Returns: { name: 'Measure', type: 'measure', ...otherProps }

// For modules not found in config, returns default structure
const unknownConfig = getModuleProps(store, "unknownModule");
// Returns: { name: 'UnknownModule', type: 'unknownModule' }
```

### Example: Determine the menu side of a module
```js
import { getModuleSide } from "@grit-npm/grit-utils";
import store from "./store";

const side = getModuleSide(store, "legend");
// => "mainMenu"

// Optional: limit the inspected sides
const custom = getModuleSide(store, "measure", { sides: ["secondaryMenu"] });
```

### Example: Wait until a Vuex module is ready
```js
import { waitForModule } from "@grit-npm/grit-utils";
import store from "./store";

// Resolve when module exists and has at least one key
const measure = await waitForModule(store, "measure");

// Wait for specific state and resolve a derived value directly
const layerList = await waitForModule(store, "layers", {
  predicate: (m) => Array.isArray(m?.list) && m.list.length > 0,
  selector: (m) => m.list,
});

// Add a timeout so calls don’t wait indefinitely
await waitForModule(store, "topics", { timeout: 10000 });
```

### Example: Check whether a module is active (with GFI handling)
```js
import { isGfiModuleActive } from "@grit-npm/grit-utils";
import store from "./store";

// Consider module active if it’s the current component or if GFI is open and matches selectors
const isActive = isGfiModuleActive(store, "measure", ["#my-gfi", ".gfi-panel"]);

// With explicit flag and custom side name
const active = isGfiModuleActive(store, "buffer", [], { flag: this.isActive, side: "mainMenu" });
```

### Example: Highlight FKZ by querying WFS around a coordinate
```js
import { handleFkzHighlighting } from "@grit-npm/grit-utils";
import store from "./store";

const coordinate = [565000, 5934000]; // EPSG:25832 [x, y]
const request = {
  url: "https://example.org/wfs",
  typename: "my:typename",
  storedQueryId: "STORED_QUERY_ID",
  outputFormat: "application/json",
}

await handleFkzHighlighting(
  store,
  coordinate,
  request,
  (data) => {
    // Use response payload for highlighting
    // e.g., parse features and update your map
  }
)
```

</details>

## Type Definitions
- JSDoc‑driven types are generated automatically after each build.
- The build runs Rollup and then tsc in declaration‑only mode, emitting `.d.ts` files to `dist`.
- We don’t use TypeScript for source — the TS step only reads JSDoc and writes definitions.

Commands
- Generate types manually: `npm run types`
- Build (JS + types): `npm run build`

Notes
- JS type checking is disabled for generation (`checkJs: false`) to keep the process non‑blocking.
- Editors pick up types via `package.json` `types` and per‑module `exports` entries.

## Development
- Install deps: `npm ci`
- Build library: `npm run build`
- Local pack test: `npm pack`
- Lint/format (Biome): `npx biome check .` and `npx biome format --write .`
- Lint ignores generated files under `dist/**`.

### Automation
- Rollup inputs are discovered automatically; any new `src/**/*.js` (except `src/index.js`) is included in the build.
- Package exports are synced automatically before each build (`prebuild`):
  - For every `src/<path>.js`, an export entry is generated:
    - key: `"./<path>"`
    - import: `"./dist/<path>.js"`
    - types: `"./dist/<path>.d.ts"`
- You can run the sync directly: `node scripts/sync-exports.js`.

## Requirements
- Node.js: 16, 18, or 20 LTS recommended.
- Peer dependencies (not bundled):
  - `ol` 9.2.4
  - `@masterportal/masterportalapi` 2.40.0
  - `axios`, `i18next`, `i18next-vue` when using related helpers

Engine note
- Using Node 22 may show an EBADENGINE warning from `@masterportal/masterportalapi` (it declares support for 16/18/20). It’s a warning only; use Node 20 LTS to silence it if desired.