How to add an SVG/d3 map to React Native? - reactjs

Use Case
I want to create a React Native app that displays coordinates stored in a PostGIS database on a world map with a special map projection.
What I have done so far
I successfully loaded the map to a React environment for my web version of the app. This is the code I implemented (only including the component that holds the map):
import React, { useEffect, useRef } from 'react';
import Aux from '../../../../hoc/Auxiliary';
import styles from './Backmap.module.css';
import * as d3 from "d3";
import { feature } from "topojson-client";
import landmass from "./../../../../land-50m";
const Backmap = (props) => {
const mapContainer = useRef(null);
let width = props.windowSize.windowWidth;
let height = props.windowSize.windowHeight;
useEffect(() => {
const svg = d3.select(mapContainer.current)
.attr("width", width)
.attr("height", height)
const g = svg.append("g")
let features = feature(landmass, landmass.objects.land).features
let projection = d3.geoAzimuthalEqualArea()
.center([180, -180])
.rotate([0, -90])
.fitSize([width, height], { type: "FeatureCollection", features: features })
.clipAngle(150)
let path = d3.geoPath().projection(projection)
g.selectAll("#landmass")
.data(features)
.enter().append("path")
.attr("id", "landmass")
.attr("d", path);
}, [])
return (
<Aux>
<svg
className={styles.Backmap}
width={props.windowSize.windowWidth}
height={props.windowSize.windowHeight}
ref={mapContainer}
>
</svg>
</Aux>
)
}
export default Backmap;
This is the resulting map from above code:
I also researched how to implement SVG shapes and even maps to React Native and came across several ways of doing so:
react-native-svg(SVG)
React Native ART(SVG)
react-native-simple-maps(map)
Problem
However, I could not implement any map using these solutions. The former two in the above list don't mention any map implementation and the latter one seems to be a very early beta version. So I am wondering if this is even currently possible in React Native. Does anybody know an approach to inserting an SVG map in React native?

It is possible to create d3 maps in react native. The thing is you cant use Dom selectors like d3.select().
You need to use react-native-svg.
Read the documentation here learn how to install and use it. Its implementaion is really close to the browser SVG API.
You import the Svg, G, and Path components
import Svg, { G, Path } from "react-native-svg";
You create a projection and path in the usual d3 manner.
let projection = d3.geoAzimuthalEqualArea()
.center([180, -180]).rotate([0, -90])
.fitSize([width, height], { type: "FeatureCollection", features: features })
.clipAngle(150)
let path = d3.geoPath().projection(projection)
The features are also extracted the same way.
let features = feature(landmass, landmass.objects.land).features
But we dont append components using d3. We append the Svg and G components directly.
return (
<Svg width='100%' height='100%'>
<G>
</G>
</Svg> )
The width and height can be styled as required.
Next the paths are appended but mapping the featurers Array to create path components.
return (
<Svg width='100%' height='100%'>
<G>
{features.map((feature, index) => {
return (<Path d={path(feature)} key={index} stroke="black" fill="grey"></Path>)
})}
</G>
</Svg> )
If you need help in rotating the svg ask in the comments to this answer.

Related

Add Custom components in #react-three/fiber scene

So my issue is that I want to add my custom components into my scene. But these custom components will be defined in a different file. So when I define my custom component all three-fiber components come across as
Cannot find name 'planeGeometry'. Did you mean 'PlaneGeometry'?
My app is written in Typescript. I tried it in sandbox for Javascript it was working fine. Maybe I didn't set up #react-three/fiber properly since even adding props to the component give error.
Say I want to add a plane mesh into my scene, Plane Mesh defined in Ground.ts and My Scene defined in Scene.ts
Scene.ts:
import {Ground} from './Ground'
const Scene =()=>{
return (
<Canvas>
<Ground />
</Canvas>
);
};
Ground.ts
import {useTexture} from '#react-three/drei';
export const Ground = () => {
const groundUrl = "<url_here>";
const texture = useTexture(groundUrl);
return (
<mesh>
<planeGeometry args={[120, 120]} />
<meshStandardMaterial map={texture} />
</mesh>
);
};
Here the components mesh, planeGeometry, meshStandardMaterial all give give errors as not found component. Is there any other step that I'm missing. I'm not very familiar with Typescript so I probably missed a few things.

How to auto center coordinates and zoom for multiple markers in react-google-maps/api

I want to show multiple dynamic markers on the map. What I mean by dynamic markers is that the markers are determined by the user's past activity. I want to determine the appropriate zoom level and center coordinates so that the user does not have to scroll through the map. 
I have checked this documentation https://react-google-maps-api-docs.netlify.app/ but can't find any example related to the specification I need.
I found an algorithm to count the center coordinate of multiple markers here Find center of multiple locations in Google Maps but only for the center coordinate (not with the best zoom level) and it's written with the original google maps api #googlemaps/js-api-loader.
How could I do search for the center coordinate and zoom for multiple markers with this #react-google-maps/api library?
https://www.npmjs.com/package/#react-google-maps/api Here in the example, they have used the onLoad function with a useCallback hook. From there, I got the understanding of use of the onLoad function to load all the markers and use the fitBounds function on markers to get the centre of all markers, and the zoom level for markers
Note: The fitBounds function is from the Google Maps library.
Here's an example:
import React from 'react';
import {GoogleMap, useLoadScript, MarkerF } from "#react-google-maps/api";
import { useSelector } from 'react-redux';
const HomePageMap = () => {
const { isLoaded } = useLoadScript({ googleMapsApiKey: "your-api-key-here" }); // store api key in env file for better security
let approvedDocs = useSelector(state => state.approvedSightingLocation.approvedSightings)
// here redux is used to store data, you can use api also to fetch data
if(!isLoaded) {
return <div>Loading...</div>
} else {
return (
<>
<div className='box-shadow' style={{margin:"20px",padding:"1px"}}>
<GoogleMap options={{streetViewControl:false,zoomControl:true}} mapContainerClassName="main-map-image"
onLoad={(map) => {
const bounds = new window.google.maps.LatLngBounds();
approvedDocs.forEach((location) => {
bounds.extend({lat:parseFloat(location.lat),lng:parseFloat(location.lng)});
})
map.fitBounds(bounds);
}}
>
{approvedDocs.map((location) => {
return <MarkerF key={location.docid} position={{lat: parseFloat(location.lat),lng: parseFloat(location.lng)}}/>
})}
</GoogleMap>
</div>
</>
)
}
}
export default HomePageMap

Mobile Safari Not Showing My Video Texture

I have a question similar to this one, but in my case, it's iOS causing troubles (not macOS, which I haven't tried yet), so I hope it's OK to post this as well. I tried to create a video texture in Three.js and can't bring it to work on mobile Safari (iOS 15.4). Here is my code, which I tried to tidy up as much as possible:
import React, { useEffect, useRef } from "react";
import * as THREE from "three";
import { Canvas } from "#react-three/fiber";
import "./styles.css";
const Screen = () => {
const meshRef = useRef();
useEffect(() => {
const vid = document.createElement("video");
vid.src = "/test.mp4";
vid.crossOrigin = "Anonymous";
vid.loop = vid.muted = vid.playsInline = true;
vid.play();
meshRef.current.material.map = new THREE.VideoTexture(vid);
});
return (
<mesh ref={meshRef}>
<planeGeometry attach="geometry" />
</mesh>
);
};
const App = () => {
return (
<Canvas camera={{ fov: 25 }}>
<Screen />
</Canvas>
);
};
export default App;
Please tell me if I'm doing something wrong here. The test.mp4 is from this URL. I also tried to place the video as HTML element, instead of creating it dynamically, then the video itself plays fine, but not the video texture.
Also, just curious, but why isn't meshRef.current available in a useEffect in the main component, but useEffect inside of Screen, which is placed inside of Canvas, is OK?
Apparently it's a problem with video file formats. Tried an example video from Three.js and it worked.
To those of you looking for the solution , you need to add
vid.playsInline=true;
for mobile ios devices.
I had the same problem. I had to set the 'playsinline' attribute in a very specific way.
video.playsinline= true did not work but video.setAttribute('playsinline', true)
did work.
Hope this helps

How to animate react-native-svg Polygon element?

Is there a simple way to animate the Polygon element from the react-native-svg library?
I need to animate his shape by animating the points.
I found few examples on how to animate Path element or Circle, but couldn't find anything regarding the Polygon. Thanks in advance.
Bit late to the party, but I've found a solution if you're still interested. It's not exactly 'simple', but it works. There's a library called React Native Reanimated, and it extends the functionality of Animated components
substantially. Here's what I was able to achieve:
The reason animating Polygons isn't available out of the box is because the standard Animated API only handles simple values, namely individual numbers. The Polygon component in react-native-svg takes props of points, which is an array of each of the points, themselves array of x and y. For example:
<Polygon
strokeWidth={1}
stroke={strokeColour}
fill={fillColour}
fillOpacity={1}
points={[[firstPointX, firstPointY],[secondPointX, secondPointY]}
/>
React Native Reanimated allows you to animate even complex data types. In this case, there is useSharedValue, which functions almost identical to new Animated.value(), and a function called useAnimatedProps, where you can create your points (or whatever else you want to animate) and pass them to the component.
// import from the library
import Animated, {
useSharedValue,
useAnimatedProps,
} from 'react-native-reanimated';
// creates the animated component
const AnimatedPolygon = Animated.createAnimatedComponent(Polygon);
const animatedPointsValues = [
{x: useSharedValue(firstXValue), y: useSharedValue(firstYValue)},
{x: useSharedValue(secondXValue), y: useSharedValue(secondYValue)},
];
const animatedProps = useAnimatedProps(() => ({
points: data.map((_, i) => {
return [
animatedPointValues[i].x.value,
animatedPointValues[i].y.value,
];
}),
})),
Then in your render/return:
<AnimatedPolygon
strokeWidth={1}
stroke={strokeColour}
fill={fillColour}
fillOpacity={1}
animatedProps={animatedProps}
/>
Then whenever you update one of those shared values, the component will animate.
I'd recommend reading their docs and becoming familiar with the library, as it will open up a whole world of possibilities:
https://docs.swmansion.com/react-native-reanimated/
Also, the animations are handled in the native UI thread, and easily hit 60fps, yet you can write them in JS.
Good luck!
react-native-reanimated also supports flat arrays for the Polygon points prop, so we can simplify the animation setup even more.
Full example which will animate the react-native-svg's Polygon when the points prop changes looks like this:
import React, { useEffect } from 'react'
import Animated, { useAnimatedProps, useSharedValue, withTiming } from 'react-native-reanimated'
import { Polygon } from 'react-native-svg'
interface Props {
points: number[]
}
const AnimatedPolygonInternal = Animated.createAnimatedComponent(Polygon)
export const AnimatedPolygon: React.FC<Props> = ({ points }: Props) => {
const sharedPoints = useSharedValue(points)
useEffect(() => {
sharedPoints.value = withTiming(points)
}, [points, sharedPoints])
const animatedProps = useAnimatedProps(() => ({
points: sharedPoints.value,
}))
return <AnimatedPolygonInternal fill="lime" animatedProps={animatedProps} />
}

How to import TrackballControls from three.js module using react?

I am using react js boiler plate (create-react-app) and imported three.js. I am trying to use TrackballControls for a particular project but it isn't working. It throws an error like "Attempted import error: 'TrackballControls' is not exported from 'three' (imported as 'THREE')" . Now I understand that it is in the examples folder and it's not an official export if I correctly understood from the forum. Some one please help me with this, how do I import TrackballControls in a react component? Help will be highly appreciated!
import React, { useState, useEffect } from 'react'
import * as THREE from 'three'
const OrbitControls = require('three-orbit-controls')(THREE)
import "../src/assets/sass/home.scss"
const X = () => {
const [y, setY] = useState(0)
const [ masses, setMasses ] = useState([])
let parent, renderer, scene, camera, controls
useEffect(() => {
// renderer
renderer = new THREE.WebGLRenderer()
renderer.setSize( window.innerWidth, window.innerHeight )
document.body.appendChild( renderer.domElement )
// scene
scene = new THREE.Scene()
// camera
camera = new THREE.PerspectiveCamera( 40, window.innerWidth / window.innerHeight, 1, 100 )
camera.position.set( 20, 20, 20 )
// controls
controls = new THREE.TrackballControls( camera, sphere )
controls.minDistance = 5
controls.maxDistance = 250
controls.enableDamping = true; // an animation loop is required when either damping or auto-rotation are enabled
controls.dampingFactor = 0.05;
// axes
// scene.add(new THREE.AxisHelper( 20 ))
// geometry
let geometry = new THREE.SphereGeometry( 2, 8, 6, 0, 6.3, 0, 3.1)
// material
let material = new THREE.MeshBasicMaterial({
wireframe: true,
wireframeLinewidth: 1
})
let sphere = new THREE.Mesh( geometry, material )
// parent
parent = new THREE.Object3D()
scene.add( parent )
scene.add( sphere )
function animate() {
requestAnimationFrame( animate )
parent.rotation.z += 0.01
controls.update()
renderer.render( scene, camera )
}
animate()
}
,[])
return <div></div>
}
export default X
All JavaScript files from the examples directory are now available as ES6 modules in the three npm package. There is actually a guide that explains how you can import them in your application:
https://threejs.org/docs/index.html#manual/en/introduction/Import-via-modules
For your particular case, it would look like so:
import { TrackballControls } from 'three/examples/jsm/controls/TrackballControls.js';
Notice that third-party packages like three-orbit-controls are not necessary anymore. When using the modules from the three package, it's guaranteed that you work with the latest (and supported) versions.
three.js R109
I know there's no one here to see this but I'm posting my solution if someone comes across this issue in the future. So here's what I figured out -
I haven't mentioned in my question that I was using a SSR (server side render) react framework called next.js. The import works perfectly fine with a pure react app. But in server side frameworks, the import related stuff should be done inside the useEffect (or componentDidMount) along with the rest of the threejs stuff. So I dynamically imported it like this -
let dynamicallyImportPackage = async () => {
let TrackballControls
await import('three/examples/jsm/controls/TrackballControls')
// you can now use the package in here
.then(module => {
TrackballControls = module.TrackballControls
})
.catch(e => console.log(e))
return TrackballControls
}
then, I used it in my useEffect like so -
let TrackbackControls = await dynamicallyImportPackage()
// controls
controls = new TrackbackControls(camera, container)
controls.minDistance = 5
controls.maxDistance = 250
Arigato Gosaimasu!

Resources