Skip to content

Commit d630de7

Browse files
authoredMar 13, 2022
Implement a Geo Json Component (#149)
* implements Polygon and MultiPolygon * fix: bugs * more GepoJsonStuff * misc: Implement other GeoJson geometries * misc: Add export for Feature aswell * misc: Add correct types * fix: Run prettier * fix: Typescript bugs * misc: Add posibility to use GeoJsonFeature
1 parent 970a4be commit d630de7

File tree

4 files changed

+334
-3
lines changed

4 files changed

+334
-3
lines changed
 

‎demo/Demo.tsx

+91-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { useState } from 'react'
22

33
import { PigeonIcon } from './PigeonIcon'
4-
import { Point, Map, Marker, Draggable, ZoomControl } from '../src'
4+
import { Point, Map, Marker, GeoJsonLoader, GeoJson, Draggable, ZoomControl } from '../src'
55
import * as providers from '../src/providers'
66

77
const markers = {
@@ -12,6 +12,79 @@ const markers = {
1212
coast: [[51.2214, 2.9541], 10],
1313
}
1414

15+
const geoJsonSample = {
16+
type: 'FeatureCollection',
17+
features: [
18+
{ type: 'Feature', geometry: { type: 'Point', coordinates: [2.0, 48.5] }, properties: { prop0: 'value0' } },
19+
{
20+
type: 'Feature',
21+
geometry: {
22+
type: 'LineString',
23+
coordinates: [
24+
[2.0, 48.0],
25+
[3.0, 49.0],
26+
[4.0, 48.0],
27+
[5.0, 49.0],
28+
],
29+
},
30+
properties: {
31+
prop0: 'value0',
32+
prop1: 0.0,
33+
},
34+
},
35+
{
36+
type: 'Feature',
37+
geometry: {
38+
type: 'Polygon',
39+
coordinates: [
40+
[
41+
[0.0, 48.0],
42+
[1.0, 48.0],
43+
[1.0, 49.0],
44+
[0.0, 49.0],
45+
[0.0, 48.0],
46+
],
47+
],
48+
},
49+
properties: {
50+
prop0: 'value0',
51+
prop1: { this: 'that' },
52+
},
53+
},
54+
{
55+
type: 'Feature',
56+
properties: { name: 'yea' },
57+
geometry: {
58+
type: 'GeometryCollection',
59+
geometries: [
60+
{ type: 'Point', coordinates: [2.0, 46.5] },
61+
{
62+
type: 'LineString',
63+
coordinates: [
64+
[2.0, 46.0],
65+
[3.0, 47.0],
66+
[4.0, 46.0],
67+
[5.0, 47.0],
68+
],
69+
},
70+
{
71+
type: 'Polygon',
72+
coordinates: [
73+
[
74+
[0.0, 46.0],
75+
[1.0, 46.0],
76+
[1.0, 47.0],
77+
[0.0, 47.0],
78+
[0.0, 46.0],
79+
],
80+
],
81+
},
82+
],
83+
},
84+
},
85+
],
86+
}
87+
1588
const lng2tile = (lon, zoom) => ((lon + 180) / 360) * Math.pow(2, zoom)
1689
const lat2tile = (lat, zoom) =>
1790
((1 - Math.log(Math.tan((lat * Math.PI) / 180) + 1 / Math.cos((lat * Math.PI) / 180)) / Math.PI) / 2) *
@@ -168,6 +241,23 @@ export function Demo(): JSX.Element {
168241
>
169242
<PigeonIcon width={100} height={95} />
170243
</Draggable>
244+
<GeoJsonLoader
245+
link="https://raw.githubusercontent.com/isellsoap/deutschlandGeoJSON/main/2_bundeslaender/4_niedrig.geo.json"
246+
styleCallback={(feature, hover) =>
247+
hover
248+
? { fill: '#93c0d099', strokeWidth: '2', stroke: 'white' }
249+
: { fill: '#d4e6ec99', strokeWidth: '1', stroke: 'white', r: '20' }
250+
}
251+
/>
252+
<GeoJson
253+
data={geoJsonSample}
254+
styleCallback={(feature, hover) => {
255+
if (feature.geometry.type === 'LineString') {
256+
return { strokeWidth: '1', stroke: 'black' }
257+
}
258+
return { fill: '#d4e6ec99', strokeWidth: '1', stroke: 'white', r: '20' }
259+
}}
260+
/>
171261
<ZoomControl />
172262
</Map>
173263
</div>

‎src/index.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ export * from './types'
22
export { Map } from './map/Map'
33
export { Marker } from './overlays/Marker'
44
export { Overlay } from './overlays/Overlay'
5+
export { GeoJson, GeoJsonLoader, GeoJsonFeature } from './overlays/GeoJson'
56
export { Draggable } from './overlays/Draggable'
67
export { ZoomControl } from './controls/ZoomControl'

‎src/map/Map.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ export class Map extends Component<MapProps, MapReactState> {
131131
maxZoom: 18,
132132
limitBounds: 'center',
133133
dprs: [],
134-
tileComponent: ImgTile
134+
tileComponent: ImgTile,
135135
}
136136

137137
_containerRef?: HTMLDivElement
@@ -1230,7 +1230,7 @@ export class Map extends Component<MapProps, MapReactState> {
12301230
<div style={boxStyle} className={boxClassname}>
12311231
<div className="pigeon-tiles" style={tilesStyle}>
12321232
{tiles.map((tile) => (
1233-
<Tile key={tile.key} tile={tile} tileLoaded={() => this.tileLoaded(tile.key)}/>
1233+
<Tile key={tile.key} tile={tile} tileLoaded={() => this.tileLoaded(tile.key)} />
12341234
))}
12351235
</div>
12361236
</div>

‎src/overlays/GeoJson.tsx

+240
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
import React, { CSSProperties, SVGProps, useMemo, useEffect, useState } from 'react'
2+
import { PigeonProps, Point } from '../types'
3+
4+
interface GeoJsonProps extends PigeonProps {
5+
className?: string
6+
data?: any
7+
svgAttributes?: any
8+
styleCallback?: any
9+
hover?: any
10+
feature?: any
11+
style?: CSSProperties
12+
children?: React.ReactNode
13+
14+
// callbacks
15+
onClick?: ({ event: HTMLMouseEvent, anchor: Point, payload: any }) => void
16+
onContextMenu?: ({ event: HTMLMouseEvent, anchor: Point, payload: any }) => void
17+
onMouseOver?: ({ event: HTMLMouseEvent, anchor: Point, payload: any }) => void
18+
onMouseOut?: ({ event: HTMLMouseEvent, anchor: Point, payload: any }) => void
19+
}
20+
21+
interface GeoJsonLoaderProps extends GeoJsonProps {
22+
link?: string
23+
}
24+
25+
interface GeoJsonGeometry {
26+
type: string
27+
coordinates?:
28+
| [number, number]
29+
| Array<[number, number]>
30+
| Array<Array<[number, number]>>
31+
| Array<Array<Array<[number, number]>>>
32+
geometries?: Array<GeoJsonGeometry>
33+
}
34+
35+
interface GeometryProps {
36+
coordinates?:
37+
| [number, number]
38+
| Array<[number, number]>
39+
| Array<Array<[number, number]>>
40+
| Array<Array<Array<[number, number]>>>
41+
latLngToPixel?: (latLng: Point, center?: Point, zoom?: number) => Point
42+
svgAttributes?: SVGProps<SVGElement>
43+
geometry?: GeoJsonGeometry
44+
}
45+
46+
const defaultSvgAttributes = { fill: '#93c0d099', strokeWidth: '2', stroke: 'white', r: '30' }
47+
48+
export function PointComponent(props: GeometryProps): JSX.Element {
49+
const { latLngToPixel } = props
50+
const [y, x] = props.coordinates as [number, number]
51+
const [cx, cy] = latLngToPixel([x, y])
52+
return <circle cx={cx} cy={cy} {...(props.svgAttributes as SVGProps<SVGCircleElement>)} />
53+
}
54+
55+
export function MultiPoint(props: GeometryProps): JSX.Element {
56+
return (
57+
<>
58+
{props.coordinates.map((point, i) => (
59+
<PointComponent {...props} coordinates={point} key={i} />
60+
))}
61+
</>
62+
)
63+
}
64+
65+
export function LineString(props: GeometryProps): JSX.Element {
66+
const { latLngToPixel } = props
67+
const p =
68+
'M' +
69+
(props.coordinates as Array<[number, number]>).reduce((a, [y, x]) => {
70+
const [v, w] = latLngToPixel([x, y])
71+
return a + ' ' + v + ' ' + w
72+
}, '')
73+
74+
return <path d={p} {...(props.svgAttributes as SVGProps<SVGPathElement>)} />
75+
}
76+
77+
export function MultiLineString(props: GeometryProps): JSX.Element {
78+
return (
79+
<>
80+
{props.coordinates.map((line, i) => (
81+
<LineString coordinates={line} key={i} />
82+
))}
83+
</>
84+
)
85+
}
86+
87+
export function Polygon(props: GeometryProps): JSX.Element {
88+
const { latLngToPixel } = props
89+
// GeoJson polygons is a collection of linear rings
90+
const p = (props.coordinates as Array<Array<[number, number]>>).reduce(
91+
(a, part) =>
92+
a +
93+
' M' +
94+
part.reduce((a, [y, x]) => {
95+
const [v, w] = latLngToPixel([x, y])
96+
return a + ' ' + v + ' ' + w
97+
}, '') +
98+
'Z',
99+
''
100+
)
101+
return <path d={p} {...(props.svgAttributes as SVGProps<SVGPathElement>)} />
102+
}
103+
104+
export function MultiPolygon(props: GeometryProps): JSX.Element {
105+
return (
106+
<>
107+
{props.coordinates.map((polygon, i) => (
108+
<Polygon {...props} coordinates={polygon} key={i} />
109+
))}
110+
</>
111+
)
112+
}
113+
114+
export function GeometryCollection(props: GeometryProps): JSX.Element {
115+
const renderer = {
116+
Point: PointComponent,
117+
MultiPoint,
118+
LineString,
119+
MultiLineString,
120+
Polygon,
121+
MultiPolygon,
122+
}
123+
124+
const { type, coordinates, geometries } = props.geometry
125+
126+
if (type === 'GeometryCollection') {
127+
return (
128+
<>
129+
{geometries.map((geometry, i) => (
130+
<GeometryCollection key={i} {...props} geometry={geometry} />
131+
))}
132+
</>
133+
)
134+
}
135+
136+
const Component = renderer[type]
137+
138+
if (Component === undefined) {
139+
console.warn(`The GeoJson Type ${type} is not known`)
140+
return null
141+
}
142+
return (
143+
<Component
144+
latLngToPixel={props.latLngToPixel}
145+
geometry={props.geometry}
146+
coordinates={coordinates}
147+
svgAttributes={props.svgAttributes}
148+
/>
149+
)
150+
}
151+
152+
export function GeoJsonFeature(props: GeoJsonProps): JSX.Element {
153+
const [internalHover, setInternalHover] = useState(props.hover || false)
154+
const hover = props.hover !== undefined ? props.hover : internalHover
155+
const callbackSvgAttributes = props.styleCallback && props.styleCallback(props.feature, hover)
156+
const svgAttributes = callbackSvgAttributes
157+
? props.svgAttributes
158+
? { ...props.svgAttributes, ...callbackSvgAttributes }
159+
: callbackSvgAttributes
160+
: props.svgAttributes
161+
? props.svgAttributes
162+
: defaultSvgAttributes
163+
164+
const eventParameters = (event: React.MouseEvent<SVGElement>) => ({
165+
event,
166+
anchor: props.anchor,
167+
payload: props.feature,
168+
})
169+
170+
return (
171+
<g
172+
clipRule="evenodd"
173+
style={{ pointerEvents: 'auto' }}
174+
onClick={props.onClick ? (event) => props.onClick(eventParameters(event)) : null}
175+
onContextMenu={props.onContextMenu ? (event) => props.onContextMenu(eventParameters(event)) : null}
176+
onMouseOver={(event) => {
177+
props.onMouseOver && props.onMouseOver(eventParameters(event))
178+
setInternalHover(true)
179+
}}
180+
onMouseOut={(event) => {
181+
props.onMouseOut && props.onMouseOut(eventParameters(event))
182+
setInternalHover(false)
183+
}}
184+
>
185+
<GeometryCollection {...props} {...props.feature} svgAttributes={svgAttributes} />
186+
</g>
187+
)
188+
}
189+
190+
export function GeoJson(props: GeoJsonProps): JSX.Element {
191+
const { width, height } = props.mapState
192+
193+
return (
194+
<div
195+
style={{
196+
position: 'absolute',
197+
left: '0',
198+
top: '0',
199+
pointerEvents: 'none',
200+
cursor: 'pointer',
201+
...(props.style || {}),
202+
}}
203+
className={props.className ? `${props.className} pigeon-click-block` : 'pigeon-click-block'}
204+
>
205+
<svg
206+
width={width}
207+
height={height}
208+
viewBox={`0 0 ${width} ${height}`}
209+
fill="none"
210+
xmlns="http://www.w3.org/2000/svg"
211+
>
212+
{props.data && props.data.features.map((feature, i) => <GeoJsonFeature key={i} {...props} feature={feature} />)}
213+
214+
{React.Children.map(props.children, (child) => {
215+
if (!child) {
216+
return null
217+
}
218+
219+
if (!React.isValidElement(child)) {
220+
return child
221+
}
222+
223+
return React.cloneElement(child, props)
224+
})}
225+
</svg>
226+
</div>
227+
)
228+
}
229+
230+
export function GeoJsonLoader(props: GeoJsonLoaderProps): JSX.Element {
231+
const [data, setData] = useState(props.data ? props.data : null)
232+
233+
useEffect(() => {
234+
fetch(props.link)
235+
.then((response) => response.json())
236+
.then((data) => setData(data))
237+
}, [props.link])
238+
239+
return data ? <GeoJson data={data} {...props} /> : null
240+
}

0 commit comments

Comments
 (0)
Please sign in to comment.