@dacostafilipe/react-geoportail 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +272 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# react-geoportail
|
|
2
|
+
|
|
3
|
+
> **Disclaimer:** This is an **unofficial, community-built** React package. It is not affiliated with, endorsed by, or supported by the [Geoportail Luxembourg](https://www.geoportail.lu) team or the Administration du Cadastre et de la Topographie (ACT). For the official API, see [apiv3.geoportail.lu](https://apiv3.geoportail.lu/proj/1.0/build/apidoc/).
|
|
4
|
+
|
|
5
|
+
An unofficial React SDK for the [Geoportail Luxembourg v3 API](https://apiv3.geoportail.lu/proj/1.0/build/apidoc/). Provides a map component and hooks for geocoding, all typed with TypeScript.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **`<GeoportailMap>`** — render a Geoportail map with optional pin/marker support
|
|
10
|
+
- **`useReverseGeocode`** — look up a Luxembourg address from lat/lon coordinates
|
|
11
|
+
- **`useGeocode`** — search for coordinates from an address string
|
|
12
|
+
- Coordinate conversion utilities (EPSG:2169 ↔ WGS84) included
|
|
13
|
+
- Zero runtime npm dependencies — uses the official `apiv3loader.js` script
|
|
14
|
+
|
|
15
|
+
## Requirements
|
|
16
|
+
|
|
17
|
+
- React 17 or later
|
|
18
|
+
- A modern browser (the Geoportail API requires it)
|
|
19
|
+
- For **production deployments**: register your domain with ACT at `support.geoportail@act.etat.lu`. The API works without restriction on `localhost`.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install @dacostafilipe/react-geoportail
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The Geoportail API script (`apiv3.geoportail.lu/apiv3loader.js`) is injected automatically — no manual `<script>` tag needed.
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
import { GeoportailMap } from '@dacostafilipe/react-geoportail';
|
|
33
|
+
|
|
34
|
+
export default function App() {
|
|
35
|
+
return (
|
|
36
|
+
<GeoportailMap
|
|
37
|
+
center={{ lat: 49.6116, lon: 6.1319 }}
|
|
38
|
+
zoom={13}
|
|
39
|
+
style={{ height: 500 }}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Components
|
|
48
|
+
|
|
49
|
+
### `<GeoportailMap>`
|
|
50
|
+
|
|
51
|
+
Renders a Geoportail map inside a `div`. The map fills its container — set a `height` via `style` or `className`.
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
<GeoportailMap
|
|
55
|
+
center={{ lat: 49.6116, lon: 6.1319 }}
|
|
56
|
+
zoom={13}
|
|
57
|
+
bgLayer="basemap_2015_global"
|
|
58
|
+
markerMode="click"
|
|
59
|
+
onMarkerPlace={(coords) => console.log(coords.lat, coords.lon)}
|
|
60
|
+
style={{ height: 480 }}
|
|
61
|
+
/>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
#### Props
|
|
65
|
+
|
|
66
|
+
| Prop | Type | Default | Description |
|
|
67
|
+
|------|------|---------|-------------|
|
|
68
|
+
| `center` | `LatLon` | Luxembourg City | Initial map center |
|
|
69
|
+
| `zoom` | `number` | `12` | Initial zoom level (1–20) |
|
|
70
|
+
| `bgLayer` | `string` | `'basemap_2015_global'` | Background layer identifier |
|
|
71
|
+
| `markerMode` | `'none' \| 'fixed' \| 'click'` | `'none'` | Controls pin behaviour (see below) |
|
|
72
|
+
| `markerPosition` | `LatLon` | — | Pin position for `'fixed'` mode, or initial pin for `'click'` mode |
|
|
73
|
+
| `onMarkerPlace` | `(coords: LatLon) => void` | — | Called when the user places a pin (`markerMode='click'`) |
|
|
74
|
+
| `layers` | `number[]` | — | Additional Geoportail layer IDs to overlay |
|
|
75
|
+
| `className` | `string` | — | CSS class for the container `div` |
|
|
76
|
+
| `style` | `React.CSSProperties` | — | Inline styles for the container `div` |
|
|
77
|
+
|
|
78
|
+
#### Marker modes
|
|
79
|
+
|
|
80
|
+
| Mode | Behaviour |
|
|
81
|
+
|------|-----------|
|
|
82
|
+
| `'none'` | No marker shown |
|
|
83
|
+
| `'fixed'` | A static pin is shown at `markerPosition` |
|
|
84
|
+
| `'click'` | User clicks the map to place/move the pin; fires `onMarkerPlace` with the clicked WGS84 coordinates |
|
|
85
|
+
|
|
86
|
+
#### Imperative ref
|
|
87
|
+
|
|
88
|
+
Pass a `ref` typed as `GeoportailMapHandle` to control the map programmatically:
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
import { useRef } from 'react';
|
|
92
|
+
import { GeoportailMap, GeoportailMapHandle } from '@dacostafilipe/react-geoportail';
|
|
93
|
+
|
|
94
|
+
const mapRef = useRef<GeoportailMapHandle>(null);
|
|
95
|
+
|
|
96
|
+
<GeoportailMap ref={mapRef} ... />
|
|
97
|
+
|
|
98
|
+
// Move the map
|
|
99
|
+
mapRef.current?.setCenter({ lat: 49.5, lon: 6.1 });
|
|
100
|
+
mapRef.current?.setZoom(15);
|
|
101
|
+
|
|
102
|
+
// Access the raw lux.Map instance
|
|
103
|
+
const luxMap = mapRef.current?.getLuxMap();
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Hooks
|
|
109
|
+
|
|
110
|
+
### `useReverseGeocode`
|
|
111
|
+
|
|
112
|
+
Convert a WGS84 lat/lon position to a Luxembourg address. Uses the Geoportail REST reverse geocode endpoint — no API key required.
|
|
113
|
+
|
|
114
|
+
```tsx
|
|
115
|
+
import { useReverseGeocode } from '@dacostafilipe/react-geoportail';
|
|
116
|
+
|
|
117
|
+
function LocationInfo({ lat, lon }: { lat: number; lon: number }) {
|
|
118
|
+
const { state, lookup } = useReverseGeocode();
|
|
119
|
+
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
lookup({ lat, lon });
|
|
122
|
+
}, [lat, lon]);
|
|
123
|
+
|
|
124
|
+
if (state.status === 'loading') return <p>Looking up address…</p>;
|
|
125
|
+
if (state.status === 'error') return <p>Error: {state.error.message}</p>;
|
|
126
|
+
if (state.status === 'success') return <p>{state.address.label}</p>;
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
#### Return value
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
const { state, lookup, reset } = useReverseGeocode();
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
| | Type | Description |
|
|
138
|
+
|-|------|-------------|
|
|
139
|
+
| `state.status` | `'idle' \| 'loading' \| 'success' \| 'error'` | Current state |
|
|
140
|
+
| `state.address` | `Address \| null` | Result when `status === 'success'` |
|
|
141
|
+
| `state.error` | `Error \| null` | Error when `status === 'error'` |
|
|
142
|
+
| `lookup(pos)` | `(pos: LatLon) => void` | Trigger a lookup; cancels any in-flight request |
|
|
143
|
+
| `reset()` | `() => void` | Return to `'idle'` and cancel any in-flight request |
|
|
144
|
+
|
|
145
|
+
#### `Address` shape
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
interface Address {
|
|
149
|
+
label: string; // Full formatted address
|
|
150
|
+
distance: number; // Distance from queried point (metres)
|
|
151
|
+
easting: number; // EPSG:2169 easting
|
|
152
|
+
northing: number; // EPSG:2169 northing
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
### `useGeocode`
|
|
159
|
+
|
|
160
|
+
Search for a Luxembourg address and get back coordinates. Supports a free-text query string or structured fields.
|
|
161
|
+
|
|
162
|
+
```tsx
|
|
163
|
+
import { useGeocode } from '@dacostafilipe/react-geoportail';
|
|
164
|
+
|
|
165
|
+
function AddressSearch() {
|
|
166
|
+
const { state, search } = useGeocode();
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<>
|
|
170
|
+
<button onClick={() => search({ queryString: 'Place d\'Armes, Luxembourg' })}>
|
|
171
|
+
Search
|
|
172
|
+
</button>
|
|
173
|
+
|
|
174
|
+
{state.status === 'success' && state.results.map((r, i) => (
|
|
175
|
+
<p key={i}>
|
|
176
|
+
{r.street} {r.num}, {r.zip} {r.locality}
|
|
177
|
+
— {r.latLon.lat.toFixed(5)}, {r.latLon.lon.toFixed(5)}
|
|
178
|
+
</p>
|
|
179
|
+
))}
|
|
180
|
+
</>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
#### Return value
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
const { state, search, reset } = useGeocode();
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
| | Type | Description |
|
|
192
|
+
|-|------|-------------|
|
|
193
|
+
| `state.status` | `'idle' \| 'loading' \| 'success' \| 'error'` | Current state |
|
|
194
|
+
| `state.results` | `GeocodeResultItem[] \| null` | Results when `status === 'success'` |
|
|
195
|
+
| `state.error` | `Error \| null` | Error when `status === 'error'` |
|
|
196
|
+
| `search(query)` | `(query: GeocodeQuery) => void` | Trigger a search; cancels any in-flight request |
|
|
197
|
+
| `reset()` | `() => void` | Return to `'idle'` |
|
|
198
|
+
|
|
199
|
+
#### `GeocodeQuery` options
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
// Free-text (recommended for most cases)
|
|
203
|
+
search({ queryString: '1 rue du Fort Thüngen, Luxembourg' });
|
|
204
|
+
|
|
205
|
+
// Structured fields
|
|
206
|
+
search({ num: '1', street: 'rue du Fort Thüngen', zip: '1499', locality: 'Luxembourg' });
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
#### `GeocodeResultItem` shape
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
interface GeocodeResultItem {
|
|
213
|
+
latLon: LatLon; // WGS84 { lat, lon }
|
|
214
|
+
easting: number; // EPSG:2169
|
|
215
|
+
northing: number; // EPSG:2169
|
|
216
|
+
accuracy: number;
|
|
217
|
+
street?: string;
|
|
218
|
+
num?: string;
|
|
219
|
+
zip?: string;
|
|
220
|
+
locality?: string;
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Coordinate utilities
|
|
227
|
+
|
|
228
|
+
The SDK exposes the internal coordinate conversion functions if you need them directly.
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
import { latLonToLuref, lurefToLatLon } from '@dacostafilipe/react-geoportail';
|
|
232
|
+
|
|
233
|
+
// WGS84 → EPSG:2169 (Luxembourg TM)
|
|
234
|
+
const { easting, northing } = latLonToLuref(49.6116, 6.1319);
|
|
235
|
+
|
|
236
|
+
// EPSG:2169 → WGS84
|
|
237
|
+
const { lat, lon } = lurefToLatLon(76651, 75358);
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Accuracy is within ~1 m across Luxembourg's territory, using a Molodensky transformation (WGS84 → ED50) followed by a Transverse Mercator projection with the official LUREF parameters.
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Development
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
# Install dependencies
|
|
248
|
+
npm install
|
|
249
|
+
|
|
250
|
+
# Start the demo app (Vite dev server)
|
|
251
|
+
npm run dev
|
|
252
|
+
|
|
253
|
+
# Type-check
|
|
254
|
+
npm run type-check
|
|
255
|
+
|
|
256
|
+
# Build the library (outputs to dist/)
|
|
257
|
+
npm run build
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
The demo app (`src/demo/main.tsx`) shows all three features together: map with click-to-pin, reverse geocoding on pin placement, and an address search bar.
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## API reference
|
|
265
|
+
|
|
266
|
+
- [Geoportail Luxembourg API v3 JSDoc](https://apiv3.geoportail.lu/proj/1.0/build/apidoc/)
|
|
267
|
+
- [REST API wiki](https://wiki.geoportail.lu/doku.php?id=en:api:rest)
|
|
268
|
+
- [Official API examples](https://apiv3.geoportail.lu/proj/1.0/build/apidoc/examples/)
|
|
269
|
+
|
|
270
|
+
## License
|
|
271
|
+
|
|
272
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dacostafilipe/react-geoportail",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "React SDK for the Geoportail Luxembourg v3 API",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/react-geoportail.umd.cjs",
|
|
7
|
+
"module": "./dist/react-geoportail.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/react-geoportail.js",
|
|
12
|
+
"require": "./dist/react-geoportail.umd.cjs",
|
|
13
|
+
"types": "./dist/index.d.ts"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"dev": "vite",
|
|
21
|
+
"build": "tsc -p tsconfig.build.json && vite build",
|
|
22
|
+
"preview": "vite preview",
|
|
23
|
+
"type-check": "tsc --noEmit"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"react": ">=17.0.0",
|
|
27
|
+
"react-dom": ">=17.0.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/react": "^18.2.0",
|
|
31
|
+
"@types/react-dom": "^18.2.0",
|
|
32
|
+
"@vitejs/plugin-react": "^4.2.0",
|
|
33
|
+
"typescript": "^5.3.0",
|
|
34
|
+
"vite": "^5.0.0",
|
|
35
|
+
"vite-plugin-dts": "^3.6.0"
|
|
36
|
+
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"geoportail",
|
|
39
|
+
"luxembourg",
|
|
40
|
+
"map",
|
|
41
|
+
"react",
|
|
42
|
+
"openlayers",
|
|
43
|
+
"geocode"
|
|
44
|
+
],
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "git+https://github.com/dacostafilipe/react-geoportail.git"
|
|
49
|
+
},
|
|
50
|
+
"author": "Filipe DA COSTA",
|
|
51
|
+
"bugs": {
|
|
52
|
+
"url": "https://github.com/dacostafilipe/react-geoportail/issues"
|
|
53
|
+
},
|
|
54
|
+
"homepage": "https://github.com/dacostafilipe/react-geoportail#readme"
|
|
55
|
+
}
|