Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,25 @@
# CGI Climate Map Challenge, Timo Ijäs

This repository was edited by Timo Ijäs @himotimo.

Instructions on how to run the application are below.

My application visualizes weather data from the Finnish Meteorological Institute API. The data is from the past seven days and is loaded on launch. The point of this application is visual and statistical analysis of climate data.

The application creates clickable markers, which open a popup. The popup shows the weather station name, a bar graph of precipitation, and line graph of climate temperature. Hover your mouse on the elements to see specific data values.

The application also features a heatmap presentation that uses temperature data. The heatmap shows a heatmap of temperatures at a certain point in time. To change the time which is represented on the heatmap, use the in the sidebar. The slider works in increments of six hours.

I intended to use inverse distance weighted interpolation to represent temperature changes between the weather station. However, implementing the method proved difficult with react-leaflet. The idea of using an interactive slider to change the visualization and analytical side of the map was intriguing, so I decided to settle with heatmap, even though it is not ideal.

As for the goals of this challenge, I have cleared parts 1, 2, and Data-Analysis part of 3. CI-pipeline is not implemented.

This was my first touch to react, HTML and Leaflet. I found the challenge inspiring and fun, and I will definitely keep practicing.



Original readme:

This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).

# CGI Climate Map Challenge
Expand Down
15,809 changes: 9,502 additions & 6,307 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,17 @@
"private": true,
"dependencies": {
"@fmidev/metolib": "^2.0.6",
"chart.js": "^2.9.4",
"leaflet": "^1.3.4",
"leaflet.idw": "0.0.1",
"react": "^16.7.0-alpha",
"react-chartjs-2": "^2.10.0",
"react-dom": "^16.7.0-alpha",
"react-input-range": "^1.3.0",
"react-leaflet": "^2.1.2",
"react-leaflet-heatmap-layer": "^2.0.0",
"react-scripts": "2.0.5",
"react-slider": "^1.1.1",
"styled-components": "^4.1.1"
},
"scripts": {
Expand Down
137 changes: 69 additions & 68 deletions src/App.js
Original file line number Diff line number Diff line change
@@ -1,87 +1,88 @@
import React, {useEffect, useState} from 'react';
import Metolib from '@fmidev/metolib';
import React, {useState} from 'react';
import './App.css';
import {Map, Marker, TileLayer} from "react-leaflet";
import styled from "styled-components";
import L from "leaflet";
import Sidebar from './Sidebar';
import WeatherMap from "./WeatherMap"
import HeatSlider from "./HeatSlider"
import HeaderText from "./HeaderText"
import IconsToLoaders from "./IconsToLoaders"
import DataGetter from "./DataGetter"
import {chartData, chartOptions} from "./DataSetup"

const MapContainer = styled(Map)`
width: calc(100vw - 300px);
height: 100vh;
position:absolute;
top:0px;
left:300px;
`;

function App() {
// Initialize state of application
// selectedLocation is id of selected location, not the json itself
const [observationLocations, setObservationLocations] = useState([]);
const [selectedLocation, setSelectedLocation] = useState(null);

// Ugly hack to fix Leaflet icons with leaflet loaders
delete L.Icon.Default.prototype._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl: require('leaflet/dist/images/marker-icon-2x.png'),
iconUrl: require('leaflet/dist/images/marker-icon.png'),
shadowUrl: require('leaflet/dist/images/marker-shadow.png'),
});
//index of date to be used for heatmap, and numerical value for date
const [dateIndex, setDateIndex] = useState(0);
const [thisDate, setThisDate] = useState(1604008800000);

// clickedLocation is the object that has been clicked
const clickedLocation = observationLocations.find(loc=> loc.info.id === selectedLocation);

function App() {
const [observationLocations, setObservationLocations] = useState([]);
// ugly way of setting up properties. Need to be fixed to useState() and useEffect()
var timeList = null;
var valListT = null;
var valListRain = null;

const [selectedLocation, setSelectedLocation] = useState(null);
// jsons for chart-js-2 properties
var data = null;
var options = null;

useEffect(function fetchObservationLocations() {
const connection = new Metolib.WfsConnection();
if (connection.connect('http://opendata.fmi.fi/wfs', 'fmi::observations::weather::cities::multipointcoverage')) {
connection.getData({
begin: Date.now() - 60e3 * 60 * 24 * 6,
end: Date.now(),
requestParameter: "t,snowdepth,r_1h",
timestep: 60 * 60 * 1000,
bbox: "20.6455928891, 59.846373196, 31.5160921567, 70.1641930203",
callback: (data, errors) => {
if (errors.length > 0) {

errors.forEach(err => {
console.error('FMI API error: ' + err.errorText);
});
return;
}

setObservationLocations(data.locations
.map(loc => {
const [lon, lat] = loc.info.position.map(parseFloat);
return {...loc, position: {lat, lon}}
})
);

connection.disconnect();
}
});
// check if loading isn't finished
const [loading, setLoading] = useState(true);
const temperatureColor= "#E87C64";
const rainColor= "#64B8E8";
const position = [65, 26];

// change heatmap properties only if data fetch is complete
if (!(observationLocations.length ===0) && !(observationLocations[0].data=== undefined)) {
var newdate= observationLocations[0].data.t.timeValuePairs[dateIndex].time;
if(thisDate!== newdate){
setThisDate(newdate)
}
}, []);
}

const position = [65, 26];
const map = (
<MapContainer center={position} zoom={6}>
<TileLayer
url='https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png'
attribution='&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> &copy; <a href="https://carto.com/attributions">CARTO</a>'
subdomains='abcd'
maxZoom={19}
/>
{observationLocations.map(loc => <Marker position={[loc.position.lat, loc.position.lon]}
key={loc.info.id} onClick={() => setSelectedLocation(loc.info.id)}>
</Marker>)}
</MapContainer>
);
if (selectedLocation) {
timeList= clickedLocation.data.t.timeValuePairs.map(row=> ''+ new Date(row.time));
valListT = clickedLocation.data.t.timeValuePairs.map(row=> row.value);
valListRain = clickedLocation.data.r_1h.timeValuePairs.map(row=> row.value);

data = chartData({timeList:timeList, temperatureColor:temperatureColor, valListT:valListT, rainColor:rainColor, valListRain:valListRain});

options = chartOptions({timeList:timeList})
}

return (
<div className="App">

<IconsToLoaders/>

<DataGetter setLoading = {setLoading} setObservationLocations={setObservationLocations}/>

<Sidebar selectedLocationId={selectedLocation} observationLocations={observationLocations}/>
{map}

<HeaderText />

<WeatherMap
position={position}
observationLocations={observationLocations}
selectedLocation={selectedLocation}
clickedLocation={clickedLocation}
setSelectedLocation={setSelectedLocation}
data={data}
options={options}
loading={loading}
dateIndex = {dateIndex}
thisDate={thisDate}
/>

<HeatSlider dateIndex = {dateIndex} setDateIndex={setDateIndex}/>

</div>
);

}

export default App;
export default App;
40 changes: 40 additions & 0 deletions src/DataGetter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {useEffect} from "react"
import Metolib from '@fmidev/metolib';

// function to get data from FMI API
function DataGetter(props){
useEffect(function fetchObservationLocations() {
const connection = new Metolib.WfsConnection();
if (connection.connect('http://opendata.fmi.fi/wfs', 'fmi::observations::weather::cities::multipointcoverage')) {
connection.getData({
begin: Date.now() - 60e3 * 60 * 24 * 6,
end: Date.now(),
requestParameter: "t,snowdepth,r_1h",
timestep: 60 * 60 * 1000,
bbox: "20.6455928891, 59.846373196, 31.5160921567, 70.1641930203",
callback: (data, errors) => {
if (errors.length > 0) {

errors.forEach(err => {
console.error('FMI API error: ' + err.errorText);
});
return;
}

props.setObservationLocations(data.locations
.map(loc => {
const [lon, lat] = loc.info.position.map(parseFloat);
return {...loc, position: {lat, lon}}
})
);

connection.disconnect();
props.setLoading(false);
}
});
}
}, []);
return (null);
}

export default DataGetter;
109 changes: 109 additions & 0 deletions src/DataSetup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// functions for setting up properties for chart-js-2 with proper parameters

function chartData(props) {
return(
{
labels: props.timeList,
datasets: [
{
label: 'Temperature: ',
type: "line",
fill: false,
lineTension: 0.1,
backgroundColor: props.temperatureColor,
borderColor: props.temperatureColor,
borderCapStyle: 'butt',
borderDashOffset: 0.0,
borderJoinStyle: 'miter',
pointBorderColor: props.temperatureColor,
pointBackgroundColor: '#fff',
pointHoverRadius: 5,
pointHoverBackgroundColor: props.temperatureColor,
pointHoverBorderColor: props.temperatureColor,
pointHoverBorderWidth: 2,
pointRadius: 1,
pointHitRadius: 10,
yAxisID: 'y-axis-2',
data: props.valListT
},
{
label: 'Precipitation: ',
type: "bar",
yAxisID: 'y-axis-1',
fill: true,
backgroundColor: props.rainColor,
borderColor: props.rainColor,
hoverBackgroundColor: props.rainColor,
hoverBorderColor: props.rainColor,
data: props.valListRain
}
]
}
)
};

function chartOptions (props) {
return (
{
responsive: true,
tooltips: {
mode: 'label'
},
elements: {
line: {
fill: false
}
},
scales: {
xAxes: [
{
type: "time",
labels: props.timeList,
time:{unit: "day"},
display: true,
gridLines: {
display: false
}
}
],
yAxes: [
{
type: 'linear',
display: true,
position: 'left',
id: 'y-axis-1',
gridLines: {
display: false
},
labels: {
show: true
},
ticks: {
min: 0,
max: 5
}
},
{
type: 'linear',
display: true,
position: 'right',
id: 'y-axis-2',
gridLines: {
display: false
},
labels: {
show: true
},
ticks: {
min: -12,
max: 15
}
}
]
}
}
);
};


export { chartData, chartOptions};
20 changes: 20 additions & 0 deletions src/HeaderText.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from "react"
// texts for header and info about application in sidebar

function HeaderText(){

return (
<div>
<h1> Weather Map</h1>
<p> This is a weather map that uses data from </p>
<p> Finnish Meteorological Institute API</p>
<p> from the past seven days.</p>
<p> Click on the markers to see temperature </p>
<p> and precipitation graphs.</p>
<p>Drag the slider to choose </p>
<p> the time for the temperature heatmap.</p>
</div>
)
}

export default HeaderText;
Loading