Compare commits

...

12 Commits

41 changed files with 3813 additions and 3501 deletions

42
.gitignore vendored
View File

@ -1,4 +1,7 @@
# ---> Node
# Others
build*
*.tar.gz
# Logs
logs
*.log
@ -6,7 +9,6 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
@ -43,8 +45,8 @@ build/Release
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
@ -55,9 +57,6 @@ web_modules/
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
@ -73,20 +72,15 @@ web_modules/
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
# dotenv environment variables file
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
@ -94,20 +88,13 @@ dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
@ -119,14 +106,3 @@ dist
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

View File

@ -1,3 +0,0 @@
# gompass
gompass

View File

@ -1,27 +0,0 @@
import React from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
/*some stylesheet is required to use react-bootstrip components*/
import 'bootstrap/dist/css/bootstrap.min.css';
import Contact from './pages/contact';
import Home from './pages/home';
import Map from './pages/map-page';
import NavBar from './components/nav-bar';
export default function App() {
return (
//BrowserRouter is the router implementation for HTML5 browsers
//Link enables Routes on an anchor tag
//Switch returns only the first matching route rather than all
//Route is the conditionally shown component //based on matching a path to a URL
<BrowserRouter>
<NavBar />
<Routes>
<Route path="/" element={<Map />} />
<Route path="/map" element={<Map />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</BrowserRouter>
);
}

View File

@ -1,35 +0,0 @@
import getIconUrl from './icon-url';
/*return icon object*/
export default function getIcon(ptByIfleet){
if(ptByIfleet===undefined || ptByIfleet===null){
//console.log('getIcon(): ptByIfleet NOT available')
return null;
}
else{
//console.log('getIcon(): ptByIfleet available')
const icon = new L.Icon({
/*path to icon graphic*/
iconUrl: getIconUrl(ptByIfleet.route_type),
/*path to graphic used for high resolution monitors*/
iconRetinaUrl: getIconUrl(ptByIfleet.route_type),
popupAnchor: [-0, -0],
/*size of the icon in width and hight*/
iconSize: [32,32],
/*determine how the popup is positions relative to the actual point on the map*/
popupAnchor:[0,-10],
/*determine how the image is positions relative to the actual point on the map*/
iconAnchor: null,
/*path to shadow graphic*/
shadowUrl: null,
/*size of the shadow in width and hight*/
shadowSize: null,
/*determine how the mage is positions relative to the actual point on the map*/
shadowAnchor: null,
className: 'marker-msg'
});
//console.log('getIcon(): icon available')
return icon;
}
}

View File

@ -1,14 +0,0 @@
import bfly from '../../assets/Logo_SIB_electricindigo.svg';
import bus from '../../assets/bus.svg';
import train from '../../assets/train.svg';
import tram from '../../assets/tram.svg';
/* return URL that fits GTFS route_type*/
export default function getIconUrl(routeTypeEnum){
//console.log('getIconUrl() routeTypeEnum: '+routeTypeEnum);
return routeTypeEnum===0?tram:
routeTypeEnum===1?bfly:
routeTypeEnum===2?train:
routeTypeEnum===3?bus:
bfly;
}

View File

@ -1,4 +0,0 @@
/*set up the height of*/
.leaflet-container {
height: 85vh;
}

View File

@ -1,39 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import {MapContainer,TileLayer} from 'react-leaflet';
/*JS module import (vs cdn or style link)*/
import 'leaflet/dist/leaflet.css'
import './map.css';
import MarkerElem from './marker-elem';
export default function Map({elements}) {
/*lat and lon of Braunschweig,DE*/
const position = [52.26594, 10.52673]
//TODO make this switch available via configuration!
const hasGtfs = false;
return (
<>
<MapContainer
center={position}
zoom={10}
minZoom={2}
scrollWheelZoom={true}
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{
elements.map(function(value,key) {
//console.log(`key: ${key}, tripId: ${value.tripId}`);
return <MarkerElem key={value.id} index={value.id} element={value}/>;
})
}
</MapContainer>
</>
);
}
Map.propTypes = {
elements: PropTypes.array
};

View File

@ -1,42 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Marker} from 'react-leaflet';
import PopupElem from './popup-elem';
import getIcon from './icon';
export default function MarkerElemPlus({element}){
if(element===undefined || element===null){
console.error('element undefined or null');
return null;
}else{
const markerIcon=getIcon();
if(markerIcon===null){
//TODO Handle issue!
console.error('ERROR: icon null');
return null;
}else if(element.lat===undefined||element.lat===null){
//TODO Handle issue!
console.error('ERROR: element lat undefined or null');
return null;
}else if(element.lon===undefined||element.lon===null){
//TODO Handle issue!
console.error('ERROR: element lon undefined or null');
return null;
}else{
return(
<>
<Marker
position={[element.lat,element.lon]}
icon={markerIcon}
>
<PopupElem element={element} />
</Marker>
</>
);
}
}
};
MarkerElemPlus.propTypes = {
element: PropTypes.object
};

View File

@ -1,22 +0,0 @@
import React, {useEffect,useState} from 'react';
import PropTypes from 'prop-types';
import MarkerElemPlus from './marker-elem-plus';
export default function MarkerElem({ element }){
if(element===undefined || element===null){
console.error('element undefined or null');
return null;
}else{
return(
<>
<MarkerElemPlus
element={element}
/>
</>
);
}
};
MarkerElem.propTypes = {
element: PropTypes.object
};

View File

@ -1,19 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Popup} from 'react-leaflet';
export default function PopupElem({element}){
return (
<>
<Popup>
id: {element.id} <br/>
name: {element.name} <br/>
lat: {element.lat} <br/>
lon: {element.lon}
</Popup>
</>
);
};
PopupElem.propTypes = {
element: PropTypes.object
};

View File

@ -1,27 +0,0 @@
import React from 'react';
import { Navbar, Nav } from 'react-bootstrap';
import { LinkContainer } from 'react-router-bootstrap';
function NavigationBar () {
return (
<Navbar collapseOnSelect fixed="top" bg="dark" expand="xxl" variant="dark">
//TODO make brand available through configuration
<Navbar.Brand href="/">GOMPASS</Navbar.Brand>
<Navbar.Toggle aria-controls="basic-navbar-nav" />
<Navbar.Collapse id="basic-navbar-nav">
<Nav className="mr-auto">
<LinkContainer to="/map">
<Nav.Link>Second Hand</Nav.Link>
</LinkContainer>
</Nav>
<Nav className="mr-auto">
<LinkContainer to="/contact">
<Nav.Link>Contact</Nav.Link>
</LinkContainer>
</Nav>
</Navbar.Collapse>
</Navbar>
);
}
export default NavigationBar;

View File

@ -1,34 +0,0 @@
import React from 'react';
import packageInfo from '../../package.json'
const VERSION = packageInfo.version;
const Contact = () => {
return (
<>
<h2>About this website</h2>
<p>
For questions about this website please do not hesitate to reach out to dialog (at) swingbe (dot) de.
</p>
<h2>Imprint</h2>
<address>
<strong>
Stefan Begerad, Software Ingenieur Begerad
</strong>
<br />
c/o WTF Kooperative eG, Hinterhaus, 3. OG
<br />
Forsmannstr. 14b
<br />
22303 Hamburg
<br />
Deutschland
<br />
</address>
<h2>Other</h2>
<p>
Version: {VERSION}
</p>
</>
);
};
export default Contact;

View File

@ -1,8 +0,0 @@
import React from 'react';
export default function Home() {
return (
<>
<h1>Home</h1>
</>
);
}

View File

@ -1,96 +0,0 @@
import React, {useEffect,useState} from 'react';
import axios from 'axios';
import qs from 'qs';
import Map from '../components/map/map';
const data = qs.stringify(
{
'data': '[bbox:51.990800124058914,10.045623779296875,52.552976197007524,11.01104736328125][out:json];(node["shop"="second_hand"];way["shop"="second_hand"];relation["shop"="second_hand"];);out center;'
}
);
//TODO Make fields available via configuration!
const interval=3600000;
const opInstance='https://overpass-api.de/api/interpreter';
const opInstanceZ='https://z.overpass-api.de/api/interpreter';
const config = {
method: 'post',
maxBodyLength: Infinity,
url: opInstanceZ,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data : data
};
function parseOsmElem(response){
let elemAry=[];
if(response.data){
let elements=response.data.elements;
let no=elements.length;
//console.log('no: '+no);
for(let i = 0; i < no; i++) {
let elemObj={};
elemObj.id=elements[i].id;
elemObj.name=elements[i].tags.name;
if(elements[i].tags.name===undefined){
//TODO Handle issue!
console.error('ERROR: OSM element does NOT include name tag');
}else if(elements[i].type==='node'){
elemObj.lat=elements[i].lat;
elemObj.lon=elements[i].lon;
}else if(elements[i].type==='way'){
elemObj.lat=elements[i].center.lat;
elemObj.lon=elements[i].center.lon;
}else{
console.error('ERROR: OSM element NOT supported');
}
//console.log('elem name: '+elemObj.name);
//console.log('elem lat: '+elemObj.lat);
//console.log('elem lon: '+elemObj.lon);
elemAry.push(elemObj);
//console.error('elemAry no: '+elemAry.length);
}
}else{
console.error('ERROR: response NOT available');
}
return elemAry;
};
export default function MapPage() {
/*storage*/
const [ary, setAry] = useState([]);
const getData= async ()=>{
//console.log('getData() start...');
try {
/*TODO handle errors: https://www.valentinog.com/blog/await-react/*/
const res = await axios(config);
let elemAry=parseOsmElem(res);
setAry((ary) => elemAry);
} catch (err) {
console.error('ERROR: err.message: ' + err.message);
}
//console.log('getData() done.');
};
useEffect(()=>{
/*do not wait the interval when component loads the first time*/
getData();
/*refresh data periodically*/
const intervalCall=setInterval(()=>{
getData();
}, interval);
/*TODO adjust interval, make it available via config file*/
return ()=>{
/*clean up*/
clearInterval(intervalCall);
};
},[]);
return (
<>
<Map elements={ary}/>
</>
);
}

View File

@ -4,7 +4,7 @@ const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
module.exports = {
//bundle *.js from this entry point
entry: path.resolve(__dirname, '../app/index.jsx'),
entry: path.resolve(__dirname, '../src/index.jsx'),
//create output file to be linked to index.html
output: {
filename: '[name].bundle.js',
@ -18,7 +18,7 @@ module.exports = {
//test all *.jsx (e.g. React.js) using babel-loader
test: /\.(js|jsx)$/,
exclude: /node_modules/,
include: path.resolve(__dirname, '../app'),
include: path.resolve(__dirname, '../src'),
use: ['babel-loader'],
},
{
@ -38,6 +38,14 @@ module.exports = {
},
],
},
{
test: /\.(png|jpe?g|gif)$/i,
use: [
{
loader: 'file-loader',
},
],
},
]
},
resolve: {

View File

@ -9,6 +9,6 @@ module.exports = merge(common, {
//enable strong source mapping
devtool: 'inline-source-map',
devServer: {
static: path.resolve(__dirname, '../build'),
static: path.resolve(__dirname, '../dist'),
},
});

5790
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,46 +1,40 @@
{
"private": true,
"name": "gompass",
"description": "gompass",
"version": "0.1.0",
"description": "Green Compass",
"version": "1.0.0",
"main": "index.js",
"keywords": [
"public",
"compass",
"gompass"
"react",
"webpack"
],
"author": "Software Ingenieur Begerad <dialog@SwIngBe.de>",
"license": "GPL-3.0-or-later",
"engines": {
"node": ">=10"
},
"scripts": {
"start": "webpack serve --config config/webpack.dev.js",
"build": "webpack --config config/webpack.prod.js"
"build": "webpack --config config/webpack.prod.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"devDependencies": {
"@babel/core": "7.19.1",
"@babel/preset-env": "7.19.1",
"@babel/preset-react": "7.18.6",
"babel-loader": "8.2.5",
"css-loader": "6.7.1",
"html-webpack-plugin": "5.5.0",
"style-loader": "3.3.1",
"@babel/core": "7.22.10",
"@babel/preset-env": "7.22.10",
"@babel/preset-react": "7.22.5",
"babel-loader": "9.1.3",
"css-loader": "6.7.3",
"file-loader": "6.2.0",
"html-webpack-plugin": "5.5.3",
"style-loader": "3.3.2",
"svg-url-loader": "8.0.0",
"webpack": "5.74.0",
"webpack-cli": "4.10.0",
"webpack-dev-server": "4.11.0",
"webpack-merge": "5.8.0"
"webpack": "5.88.2",
"webpack-cli": "5.1.4",
"webpack-dev-server": "4.15.1",
"webpack-merge": "5.9.0"
},
"dependencies": {
"axios": "0.27.2",
"bootstrap": "5.2.1",
"leaflet": "1.8.0",
"qs": "^6.11.0",
"leaflet": "1.9.4",
"prop-types": "15.8.1",
"react": "18.2.0",
"react-bootstrap": "2.5.0",
"react-dom": "18.2.0",
"react-leaflet": "4.0.2",
"react-router-bootstrap": "0.26.2"
"react-router-dom": "6.16.0"
}
}

View File

@ -2,18 +2,6 @@
<html>
<head>
<meta charset="UTF-8">
<!-- load resources for bootstrap -->
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3"
crossorigin="anonymous">
<!-- avoid content hiding behind react-bootstrap navbar -->
<style type="text/css">
body {
padding-top: 100px;
}
</style>
<title>Gompass</title>
</head>
<body>

20
readme.md Normal file
View File

@ -0,0 +1,20 @@
# init
```
mkdir ~/git && cd ~/git
git clone -b main <repository>
cd <repository>
npm i
```
# dev
```
npm run start
```
# prod
```
npm run build
```

View File

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -0,0 +1,67 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { get } from '../utils/request';
//TODO adjust config usage
import config from '../utils/config';
import Map from './map';
import Table from './table';
/*destructure props object*/
export default function FetchOsm ({address, title}) {
const [array, setArray] = useState([]);
/*fetch array in a JavaScript function*/
const fetch = async () => {
if(config && address){
try {
/*TODO handle errors: https://www.valentinog.com/blog/await-react/*/
//console.log("Fetch:fetch(): address: " + address);
const res = await get(address);
//console.log("Fetch:fetch(): res.length: "+res.elements.length);
setArray((array) => res.elements);
} catch (err) {
console.error('err.message: ' + err.message);
setArray((array) => []);
}
}else{
//console.log("Fetch:fetch(): address: " + address);
console.error('Fetch:fetch(): config or address NOT available');
}
};
useEffect(() => {
/*effect goes here*/
fetch();
/*use an empty dependency array to ensure the hook is running only once*/
/*TODO study dependency array: https://reactjs.org/docs/hooks-effect.html*/
}, []);
if ( array !== undefined && array !== null && array.length > 0 ) {
/*return a React element*/
return (
<>
<Map
array={array}
title={title}
/>
<Table
array={array}
title={title}
/>
</>
);
}else{
return (
<>
<p>Loading...</p>
</>
);
}
};
FetchOsm.propTypes = {
address: PropTypes.string,
title: PropTypes.string
};

31
src/components/header.jsx Normal file
View File

@ -0,0 +1,31 @@
import React from 'react'
import { Link} from 'react-router-dom';
export default function Header(){
return (
<>
<Link
to='/'
>
<button>
Home
</button>
</Link>
<Link
to='https://www.swingbe.de/imprint/'
>
<button>
Imprint
</button>
</Link>
<Link
to='https://www.swingbe.de/privacy-policy/'
>
<button>
Privacy Policy
</button>
</Link>
</>
);
};

View File

@ -1,9 +1,8 @@
import L from "leaflet";
import bfly from '../../assets/Logo_SIB_electricindigo.svg';
/*return icon object*/
export default function getIcon(){
//console.log('getIcon(): ptByIfleet available')
const icon = new L.Icon({
export const icon = new L.icon({
/*path to icon graphic*/
iconUrl: bfly,
/*path to graphic used for high resolution monitors*/
@ -23,6 +22,3 @@ export default function getIcon(){
shadowAnchor: null,
className: 'marker-msg'
});
//console.log('getIcon(): icon available')
return icon;
}

View File

@ -0,0 +1,5 @@
import L from "leaflet";
import iconUrl from "leaflet/dist/images/marker-icon.png";
export const icon = L.icon({
iconUrl,
});

View File

@ -0,0 +1,71 @@
import React, { useEffect, useState } from 'react';
import MapObject from './map-object';
import { get } from '../utils/request';
// import JSON
import jsonOsmQuery from '../osm-query.json';
export default function JsonOsmQuery(){
const rryOsmQuery = jsonOsmQuery.queries;
console.log('rry.lngth: ' + rryOsmQuery.length);
const [mp, setMp] = useState(null);
/*fetch array in a JavaScript function*/
const fetch = async () => {
try {
const mpRsp = new Map()
console.log('mpRsp.lngth: ' + mpRsp.size);
for ( let i = 0; i < rryOsmQuery.length; i++ ) {
console.log('i: ' + i );
const query = rryOsmQuery[i];
console.log('query title: ' + query.title);
const address = query.query;
console.log("Fetch:fetch(): address: " + address);
const res = await get(address);
console.log("Fetch:fetch(): res.length: "+res.elements.length);
mpRsp.set(query.title, res.elements)
}
setMp((mp) => mpRsp);
} catch (err) {
console.error('err.message: ' + err.message);
setMp((array) => null);
}
};
useEffect(() => {
/*effect goes here*/
fetch();
/*use an empty dependency array to ensure the hook is running only once*/
/*TODO study dependency array: https://reactjs.org/docs/hooks-effect.html*/
}, []);
if ( mp !== undefined && mp !== null && mp.size > 0 ) {
/*return a React element*/
return (
<>
<p>JsonOsmQuery</p>
<MapObject
object={mp}
/>
</>
);
}else{
return (
<>
<p>JsonOsmQuery</p>
<p>Loading...</p>
</>
);
}
};

103
src/components/list.js Normal file
View File

@ -0,0 +1,103 @@
import React from 'react'
import { Link} from 'react-router-dom';
export default function List(){
return (
<>
<ul>
<li>
<Link to={'/bike-ride'} >
<button>
Bike and ride (B+R) stations
</button>
</Link>
</li>
<li>
<Link to={'/organic-bakery'} >
<button>
Organic bakeries
</button>
</Link>
</li>
<li>
<Link to={'/organic-only-shop'} >
<button>
Organic only shops
</button>
</Link>
</li>
<li>
<Link to={'/organic-shop'} >
<button>
Organic shops
</button>
</Link>
</li>
<li>
<Link to={'/park-ride'} >
<button>
Park and ride (P+R) stations
</button>
</Link>
</li>
<li>
<Link to={'/public-bookcase'} >
<button>
Public bookcases
</button>
</Link>
</li>
<li>
<Link to={'/second-hand'} >
<button>
Second hand shops
</button>
</Link>
</li>
<li>
<Link to={'/taxi'} >
<button>
Taxi stations
</button>
</Link>
</li>
<li>
<Link to={'/ticket-machine'} >
<button>
Ticket machines
</button>
</Link>
</li>
<li>
<Link to={'/ticket-office'} >
<button>
Ticket offices
</button>
</Link>
</li>
<li>
<Link to={'/vegan'} >
<button>
Vegan only restaurants
</button>
</Link>
</li>
<li>
<Link to={'/vegetarian'} >
<button>
Vegetarian only restaurants
</button>
</Link>
</li>
<li>
<Link to={'/zero-waste'} >
<button>
Zero waste shops
</button>
</Link>
</li>
</ul>
</>
);
};

133
src/components/map-array.js Normal file
View File

@ -0,0 +1,133 @@
import L from 'leaflet'
import PropTypes from 'prop-types';
import React, { useEffect, useRef, useState } from 'react'
import "leaflet/dist/leaflet.css";
import icon from "leaflet/dist/images/marker-icon.png";
import iconShadow from "leaflet/dist/images/marker-shadow.png";
let DefaultIcon = L.icon({
iconUrl: icon,
shadowUrl: iconShadow,
});
export default function MapArray( { array } ) {
console.log('map-array:MapArray: array.lngth: ' + array.length);
/*center map at Node: Umweltzentrum Braunschweig (3650315388)*/
const mapCenter = [52.2671189, 10.5221905];
const mapRef = useRef();
useEffect(() => {
//base layer
const baseLayerOsm = L.tileLayer(
"http://{s}.tile.osm.org/{z}/{x}/{y}.png",
{
attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors',
maxZoom: 18,
scrollWheelZoom: false,
}
);
//marker array
const rryMrkr = array.map( function( value, key ) {
//console.log('map-array:MapArray:map() value: ' +JSON.stringify(value));
//get entity properties
const id = value.id;
const type = value.type;
const name = value.tags.name;
let lat, lon = null;
if ( type === "node"){
lat = value.lat;
lon = value.lon;
} else if ( type === "way" || type === "relation" ) {
lat = value.center.lat;
lon = value.center.lon;
} else {
console.error("map-entities: OSM type (" + type + ") NOT known");
}
//verify entity properties
if(lat !== undefined && lat !== null && lon !== undefined && lon !== null ){
//TODO
/*
var marker = L.marker([lat, lon]).addTo(map);
const popupText ='name: <b>' + name + '</b><br />id: ' + id + '<br />type: ' + type;
marker.bindPopup(popupText);
*/
L.Marker.prototype.options.icon = DefaultIcon;
const marker = L.marker(
[lat, lon],
{alt: 'marker name: ' + name}
);
const popupText ='name: <b>' + name + '</b><br />lat: ' + lat + '<br />lon: ' + lon;
//console.log('popupText: ' + popupText);
marker.bindPopup(popupText);
return marker
} else {
console.error('map-entities: lat or lon undefined or null');
}
});
console.log('rryMrkr: ' + rryMrkr.length);
//layer group
const overlayMrkr = L.layerGroup(rryMrkr);
//map
const map = L.map(
mapRef.current,
{attributionControl: true,
layers : [baseLayerOsm, overlayMrkr]}
).setView(
mapCenter,
10
);
//layout control
var baseMaps = {
"OpenStreetMap": baseLayerOsm
};
var overlayMaps = {
'Markers' : overlayMrkr
};
const layerControl = L.control.layers(baseMaps, overlayMaps).addTo(map);
// unmount map function
//You should unmount the function in react.js to remove the existing map.
return () => map.remove();
}, []);
if ( array !== undefined && array !== null && array.length > 0 ) {
return (
<>
<p>MapArray</p>
<div
style={{padding: 0, margin: 0, width: "75%", height: "23vh",}}
ref={el => mapRef.current = el}>
</div>
</>
);
}else{
//TODO Why does mapRef have to be present?
return (
<>
<p>MapArray loading...</p>
<div
ref={el => mapRef.current = el}>
</div>
</>
);
}
};
//https://github.com/azaharyan/react-leaflet-example
MapArray.propTypes = {
array: PropTypes.array
};

View File

@ -0,0 +1,54 @@
import React, { useEffect, useState } from 'react';
import { get } from '../utils/request';
//TODO adjust config usage
import config from '../utils/config';
import Map from './map-array';
export default function MapLGroup(){
const [array, setArray] = useState([]);
/*fetch array in a JavaScript function*/
const fetch = async () => {
try {
/*TODO handle errors: https://www.valentinog.com/blog/await-react/*/
const address = config.ADDRESS + 'nwr[shop=second_hand](area)->.shops;nwr[second_hand=yes](area)->.secondHands;(.shops;.secondHands;);' + config.OUT;
console.log("Fetch:fetch(): address: " + address);
const res = await get(address);
console.log("Fetch:fetch(): res.length: "+res.elements.length);
setArray((array) => res.elements);
} catch (err) {
console.error('err.message: ' + err.message);
setArray((array) => []);
}
};
useEffect(() => {
/*effect goes here*/
fetch();
/*use an empty dependency array to ensure the hook is running only once*/
/*TODO study dependency array: https://reactjs.org/docs/hooks-effect.html*/
}, []);
if ( array !== undefined && array !== null && array.length > 0 ) {
/*return a React element*/
return (
<>
<p>MapLGroup</p>
<Map
array={array}
/>
</>
);
}else{
return (
<>
<p>MapLGroup</p>
<p>Loading...</p>
</>
);
}
}

View File

@ -0,0 +1,153 @@
import L from 'leaflet'
import PropTypes from 'prop-types';
import React, { useEffect, useRef, useState } from 'react'
import "leaflet/dist/leaflet.css";
import icon from "leaflet/dist/images/marker-icon.png";
import iconShadow from "leaflet/dist/images/marker-shadow.png";
let DefaultIcon = L.icon({
iconUrl: icon,
shadowUrl: iconShadow,
});
export default function MapObject( { object } ) {
console.log('map-object:MapObject: object.size: ' + object.size);
/*center map at Node: Umweltzentrum Braunschweig (3650315388)*/
const mapCenter = [52.2671189, 10.5221905];
const mapRef = useRef();
useEffect(() => {
//base layer
const baseLayerOsm = L.tileLayer(
"http://{s}.tile.osm.org/{z}/{x}/{y}.png",
{
attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors',
maxZoom: 18,
scrollWheelZoom: false,
}
);
//map
const map = L.map(
mapRef.current,
{attributionControl: true,
layers : [baseLayerOsm]}
).setView(
mapCenter,
10
);
const rryTitleKeys = new Array();
const rryLayerValues = new Array();
//iterate over osm rsp objects
for (const [title, elements] of object) {
console.log(`The value for key ${title} is ${elements}`);
//add title
rryTitleKeys.push(title);
//add layer
//marker array
const rryMrkr = elements.map( function( value, key ) {
//console.log('map-object:MapObject:map() value: ' +JSON.stringify(value));
//get entity properties
const id = value.id;
const type = value.type;
const name = value.tags.name;
let lat, lon = null;
if ( type === "node"){
lat = value.lat;
lon = value.lon;
} else if ( type === "way" || type === "relation" ) {
lat = value.center.lat;
lon = value.center.lon;
} else {
console.error("map-entities: OSM type (" + type + ") NOT known");
}
//verify entity properties
if(lat !== undefined && lat !== null && lon !== undefined && lon !== null ){
//TODO
/*
var marker = L.marker([lat, lon]).addTo(map);
const popupText ='name: <b>' + name + '</b><br />id: ' + id + '<br />type: ' + type;
marker.bindPopup(popupText);
*/
L.Marker.prototype.options.icon = DefaultIcon;
const marker = L.marker(
[lat, lon],
{alt: 'marker name: ' + name}
);
const popupText ='name: <b>' + name + '</b><br />lat: ' + lat + '<br />lon: ' + lon;
//console.log('popupText: ' + popupText);
marker.bindPopup(popupText);
return marker
} else {
console.error('map-entities: lat or lon undefined or null');
}
});
console.log('rryMrkr: ' + rryMrkr.length);
//layer group
const overlayMrkr = L.layerGroup(rryMrkr);
rryLayerValues.push(overlayMrkr);
}
console.log('rryLayerValues.lngth: ' + rryLayerValues.length);
console.log('rryTitleKeys.lngth: ' + rryTitleKeys.length);
const overlayMaps = new Object();
for (let i = 0; i < rryTitleKeys.length; i++) {
const ttl = rryTitleKeys[i];
const lyr = rryLayerValues[i];
console.log('i: ' + i + ', ttl: ' + ttl);
overlayMaps[ttl] = lyr;
}
//ERROR console.log('overlayMaps: ' + JSON.stringify(overlayMaps));
//layout control
const baseMaps = {
"OpenStreetMap": baseLayerOsm
};
const layerControl = L.control.layers(baseMaps, overlayMaps).addTo(map);
// unmount map function
//You should unmount the function in react.js to remove the existing map.
return () => map.remove();
}, []);
if ( object !== undefined && object !== null && object.size > 0 ) {
return (
<>
<p>MapObject</p>
<div
style={{padding: 0, margin: 0, width: "75%", height: "23vh",}}
ref={el => mapRef.current = el}>
</div>
</>
);
}else{
//TODO Why does mapRef have to be present?
return (
<>
<p>MapObject loading...</p>
<div
ref={el => mapRef.current = el}>
</div>
</>
);
}
};
//https://github.com/azaharyan/react-leaflet-example
MapObject.propTypes = {
array: PropTypes.array
};

28
src/components/map.jsx Normal file
View File

@ -0,0 +1,28 @@
import React from 'react';
import PropTypes from 'prop-types';
import EntitiesMap from './map/map-entities';
/*destructure props object*/
export default function Map ({array, title}){
if ( array !== undefined && array !== null && array.length > 0 ) {
/*return a React element*/
return (
<>
<p>Map of {array.length}&nbsp;{title}</p>
<EntitiesMap entities={array} />
</>
);
}else{
return (
<>
<p>Loading...</p>
</>
);
}
};
Map.propTypes = {
array: PropTypes.array,
title: PropTypes.string
};

View File

@ -0,0 +1,99 @@
import React, { useEffect, useRef, useState } from 'react'
import PropTypes from 'prop-types';
import L from 'leaflet'
import "leaflet/dist/leaflet.css";
import { icon } from "../icons/icon-bfly";
//maxBounds={[[-60, -180], [85, 180]]}
const corner1 = L.latLng(-60, -180),
corner2 = L.latLng(85, 180),
bounds = L.latLngBounds(corner1, corner2);
export default function MapEntities( { entities } ) {
//console.log('map-entities:MapEntities: entities.lngth: ' + entities.length);
/*center map at Node: Umweltzentrum Braunschweig (3650315388)*/
const mapCenter = [52.2671189, 10.5221905];
const mapRef = useRef();
useEffect(() => {
const map = L.map( mapRef.current, {attributionControl: true})
.setView( mapCenter, 12).
setMaxBounds( bounds );
L.tileLayer(
"http://{s}.tile.osm.org/{z}/{x}/{y}.png",
{
attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors',
minZoom: 4,
maxZoom: 18,
scrollWheelZoom: false,
}).addTo(map);
//TODO: Why is 'scrollWheelZoom: false,' above not working?
//TODO: Why the following line instead?
map.scrollWheelZoom.disable()
entities.map( function( value, key ) {
//console.log('map-entities:MapEntities:map() value: ' +JSON.stringify(value));
L.Marker.prototype.options.icon = icon;
//get entity properties
const id = value.id;
const type = value.type;
const name = value.tags.name;
let lat, lon = null;
if ( type === "node"){
lat = value.lat;
lon = value.lon;
} else if ( type === "way" || type === "relation" ) {
lat = value.center.lat;
lon = value.center.lon;
} else {
console.error("map-entities: OSM type (" + type + ") NOT known");
}
//verify entity properties
if(lat !== undefined && lat !== null && lon !== undefined && lon !== null ){
var marker = L.marker([lat, lon]).addTo(map);
const popupText ='name: <b>' + name + '</b><br />id: ' + id + '<br />type: ' + type;
marker.bindPopup(popupText);
} else {
console.error('map-entities: lat or lon undefined or null');
}
});
// unmount map function
//You should unmount the function in react.js to remove the existing map.
return () => map.remove();
}, []);
if ( entities !== undefined && entities !== null && entities.length > 0 ) {
return (
<div
style={{padding: 0, margin: 0, height: "75vh",}}
ref={el => mapRef.current = el}>
</div>
);
}else{
//TODO Why does mapRef have to be present?
return (
<>
<div
ref={el => mapRef.current = el}>
</div>
<p>MapEntities loading...</p>
</>
);
}
};
MapEntities.propTypes = {
entities: PropTypes.array
};

View File

@ -0,0 +1,42 @@
import React from 'react';
import PropTypes from 'prop-types';
function TableEntries ({array}) {
if ( array !== undefined && array !== null && array.length > 0 ) {
//iterate over array
return array.map((item, index) => {
if( item.type === "node"){
return (
<tr
key={item.id}
>
<td>{item.id}</td>
<td>{item.tags.name}</td>
<td>{item.lat}</td>
<td>{item.lon}</td>
</tr>
);
} else if( item.type === "way" || item.type === "relation") {
return (
<tr
key={item.id}
>
<td>{item.id}</td>
<td>{item.tags.name}</td>
<td>{item.center.lat}</td>
<td>{item.center.lon}</td>
</tr>
);
} else {
console.error("table-entities: OSM type NOT known");
}
});
}else{
//data is empty
return null;
}
}
TableEntries.propTypes = {
array: PropTypes.array
};
export default TableEntries;

40
src/components/table.jsx Normal file
View File

@ -0,0 +1,40 @@
import React from 'react';
import PropTypes from 'prop-types';
import TableEntries from './table-entries';
/*destructure props object*/
export default function Table ({array, title}){
if ( array !== undefined && array !== null && array.length > 0 ) {
/*return a React element*/
return (
<>
<p>Table of {array.length}&nbsp;{title}</p>
<table>
<thead>
<tr>
<th>id</th>
<th>name</th>
<th>lon</th>
<th>lat</th>
</tr>
</thead>
<tbody>
<TableEntries array={array} />
</tbody>
</table>
</>
);
}else{
return (
<>
<h1>Table of {title}</h1>
<p>Table loading...</p>
</>
);
}
};
Table.propTypes = {
array: PropTypes.array,
title: PropTypes.string
};

View File

@ -1,14 +1,21 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './app';
import ReactDOM from 'react-dom';
import Main from './main';
//TODO remove debugging
if (process.env.NODE_ENV !== 'production') {
console.log('development mode');
}
const container = document.getElementById('root');
const root = createRoot(container);
//since react 18
import { createRoot } from 'react-dom/client';
//create root container
const root = createRoot(document.getElementById("root"));
//render root main
root.render(
<React.StrictMode>
<App tab="home" />
<Main />
</React.StrictMode>
);

68
src/main.jsx Normal file
View File

@ -0,0 +1,68 @@
import React from 'react';
import { BrowserRouter as Router, Link, Route, Routes } from 'react-router-dom';
import EntityOsm from './pages/entity-osm';
import Header from './components/header';
import Home from './pages/home';
import packageInfo from '../package.json'
import config from './utils/config';
const VERSION = packageInfo.version;
const area = '(area);out body center qt;';
export default function App(){
return (
<Router>
<Header />
<h1>Gompass</h1>
<Routes>
<Route path='/' element={<Home/>}/>
<Route path='/bike-ride' element={<EntityOsm
address={config.ADDRESS + 'nwr[amenity=bicycle_parking][bike_ride]["bike_ride"!="no"]' + config.AREA }
title='B+R Stations'/>}/>
<Route path='/organic-bakery' element={<EntityOsm
address={ config.ADDRESS + 'nwr[shop=bakery][organic=yes]' + config.AREA }
title='Orgnic Bakeries'/>}/>
<Route path='/organic-only-shop' element={<EntityOsm
address={ config.ADDRESS + 'nwr[shop][organic=only]' + config.AREA }
title='Orgnic Only Shops'/>}/>
<Route path='/organic-shop' element={<EntityOsm
address={ config.ADDRESS + 'nwr[shop][organic=yes]' + config.AREA }
title='Orgnic Shops'/>}/>
<Route path='/park-ride' element={<EntityOsm
address={ config.ADDRESS + 'nwr["park_ride"!="no"]["park_ride"]' + config.AREA }
title='P+R Stations'/>}/>
<Route path='/public-bookcase' element={<EntityOsm
address={ config.ADDRESS + 'nwr[amenity=public_bookcase]' + config.AREA }
title='Public Bookcases'/>}/>
<Route path='/second-hand' element={<EntityOsm
address={ config.ADDRESS + 'nwr[shop=second_hand](area)->.shops;nwr[second_hand=yes](area)->.secondHands;(.shops;.secondHands;);' + config.OUT }
title='Second Hand Shops'/>}/>
<Route path='/taxi' element={<EntityOsm
address={ config.ADDRESS + 'nwr[amenity=taxi]' + config.AREA }
title='Taxi Stations'/>}/>
<Route path='/ticket-machine' element={<EntityOsm
address={ config.ADDRESS + 'nwr[amenity=vending_machine][vending=public_transport_tickets]' + config.AREA }
title='Ticket Machines'/>}/>
<Route path='/ticket-office' element={<EntityOsm
address={ config.ADDRESS + 'nwr[shop=ticket]["tickets:public_transport"!=no]' + config.AREA }
title='Ticket Offices'/>}/>
<Route path='/vegan' element={<EntityOsm
address={ config.ADDRESS + 'nwr[amenity=restaurant]["diet:vegan"=only]' + config.AREA }
title='Vegan Only Restaurants'/>}/>
<Route path='/vegetarian' element={<EntityOsm
address={ config.ADDRESS + 'nwr[amenity=restaurant]["diet:vegetarian"=only]' + config.AREA }
title='Vegetarian Only Restaurants'/>}/>
<Route path='/zero-waste' element={<EntityOsm
address={ config.ADDRESS + 'nwr[bulk_purchase=yes](area)->.bp;nwr[zero_waste=yes](area)->.zw;(.bp;.zw;);' + config.OUT }
title='Zero Waste Shops'/>}/>
</Routes>
<p>
Version:&nbsp;{VERSION}
</p>
</Router>
);
}

20
src/osm-query.json Normal file
View File

@ -0,0 +1,20 @@
{
"queries": [
{
"title": "Second hand shops",
"query": "https://overpass-api.de/api/interpreter?data=[out:json][timeout:60];relation(62531);map_to_area;nwr[shop=second_hand](area)->.shops;nwr[second_hand=yes](area)->.secondHands;(.shops;.secondHands;);out body center qt;"
},
{
"title": "Organic bakeries",
"query": "https://overpass-api.de/api/interpreter?data=[out:json][timeout:60];relation(62531);map_to_area;nwr[shop=bakery][organic=yes](area);out body center qt;"
},
{
"title": "Organic only shops",
"query": "https://overpass-api.de/api/interpreter?data=[out:json][timeout:60];relation(62531);map_to_area;nwr[shop][organic=only](area);out body center qt;"
},
{
"title": "Public bookcases",
"query": "https://overpass-api.de/api/interpreter?data=[out:json][timeout:60];relation(62531);map_to_area;nwr[amenity=public_bookcase](area);out body center qt;"
}
]
}

19
src/pages/entity-osm.jsx Normal file
View File

@ -0,0 +1,19 @@
import React from 'react'
import PropTypes from 'prop-types';
import FetchOsm from '../components/fetch-osm';
export default function EntityOsm({address, title}) {
return(
<>
<FetchOsm
address={address}
title={title}
/>
</>
);
};
EntityOsm.propTypes = {
address: PropTypes.string,
title: PropTypes.string
};

17
src/pages/home.jsx Normal file
View File

@ -0,0 +1,17 @@
import React from 'react'
import { Link} from 'react-router-dom';
import List from '../components/list';
import MapLGroup from '../components/map-lgroup';
import JsonOsmQuery from '../components/json-osm-query';
export default function Home(){
return (
<>
<List />
<MapLGroup />
<JsonOsmQuery />
</>
);
}

6
src/utils/config.js Normal file
View File

@ -0,0 +1,6 @@
export default {
API: 'https://v1rvb.api.swingbe.de/',
ADDRESS: 'https://overpass-api.de/api/interpreter?data=[out:json][timeout:60];relation(62531);map_to_area;',
AREA: '(area);out body center qt;',
OUT: 'out body center qt;',
};

35
src/utils/request.js Normal file
View File

@ -0,0 +1,35 @@
/**
* get Wikidata content as string
* @param unique q Wikidata content identifier
* @return Wikidata content as string
*/
export async function getWikidata(q) {
//console.log('request:getWikidata() q: ' + q);
const address = 'https://www.wikidata.org/w/rest.php/wikibase/v0/entities/items/' + q;
//console.log('request:getWikidata() address: ' + address)
const objct = await get(address);
////console.log('request:getWikidata() objct: ' + JSON.stringify(objct));
//const strng = JSON.stringify(objct);
////console.log('request:getWikidata() strng: ' + strng);
//return strng;
return objct
};
/**
* http get request
*
* @param pth path
* @return response as JSON data
*/
export async function get(pth) {
//console.log('request:get() pth: ' + pth);
const data = await fetch(pth, {
method: 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
const objct = await data.json();
return objct;
};