Compare commits

...

10 Commits

Author SHA1 Message Date
b2eae74b9e some improvements 2020-07-08 11:44:05 +03:00
167a608b93 remove persisted context 2020-05-25 21:43:45 +03:00
86f632d72b deleted some useless stuff and backend integration 2020-05-25 21:43:19 +03:00
e1abcdb379 remove yarn lock 2020-05-25 21:01:36 +03:00
81bf9dc585 crazy 2020-05-25 20:59:33 +03:00
2e4f90d8ff add puppeteer options 2020-05-25 16:46:32 +03:00
32b3c643ef disable automatic alpr for now 2020-05-25 16:39:45 +03:00
cc0b5efe9f creating cache directories 2020-05-25 16:35:46 +03:00
d986f7801b update deps 2020-05-25 16:25:46 +03:00
546d3d760e babel as regular dependency for heroku 2020-05-25 16:23:37 +03:00
49 changed files with 7094 additions and 22853 deletions

7118
api/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,40 +1,43 @@
{
"name": "maanteeamet-fetch",
"name": "car-fetcher-api",
"version": "1.0.0",
"main": "src/index.js",
"scripts": {
"start": "babel-node src/index.js",
"start": "cross-env NODE_ENV=production babel-node src/index.js",
"dev": "nodemon -r dotenv/config --watch src --exec babel-node src/index.js"
},
"engines": {
"node": "14.3.0",
"npm": "6.14.5"
"node": "^14",
"npm": "^6.14"
},
"dependencies": {
"@babel/cli": "7.10.4",
"@babel/core": "7.10.4",
"@babel/node": "7.10.4",
"@babel/plugin-proposal-class-properties": "7.10.4",
"@babel/preset-env": "7.10.4",
"body-parser": "1.19.0",
"cacheman": "2.2.1",
"cacheman-file": "0.2.1",
"compression": "1.7.4",
"cross-env": "7.0.2",
"dotenv": "8.2.0",
"express": "4.17.1",
"form-data": "3.0.0",
"helmet": "^3.22.0",
"helmet": "3.23.3",
"jsdom": "16.2.2",
"multer": "1.4.2",
"node-fetch": "2.6.0",
"puppeteer": "3.1.0"
"puppeteer": "5.0.0"
},
"devDependencies": {
"@babel/cli": "7.8.4",
"@babel/core": "7.9.6",
"@babel/node": "7.8.7",
"@babel/plugin-proposal-class-properties": "7.8.3",
"@babel/preset-env": "7.9.6",
"babel-eslint": "10.1.0",
"eslint": "^6.8.0",
"eslint-config-airbnb": "18.1.0",
"eslint-plugin-import": "2.20.2",
"eslint-plugin-jsx-a11y": "6.2.3",
"eslint-plugin-react": "7.20.0",
"eslint-plugin-react-hooks": "2.5.0",
"eslint": "7.4.0",
"eslint-config-airbnb": "18.2.0",
"eslint-plugin-import": "2.22.0",
"eslint-plugin-jsx-a11y": "6.3.1",
"eslint-plugin-react": "7.20.3",
"eslint-plugin-react-hooks": "4.0.6",
"nodemon": "2.0.4"
}
}

View File

@@ -5,8 +5,17 @@ import {
TEMP_DIR,
SITE_COOKIES,
} from '../util/Constants';
import Selectors from '../util/Selectors';
import Selectors, { SUBMIT_FORM_ID } from '../util/Selectors';
import { createCacheDirectories } from '../util/TempDirCreator';
const getPuppeteerOptions = () => {
const options = {};
if (process.env.NODE_ENV === 'production') {
options.args = ['--no-sandbox'];
}
return options;
};
console.log(Selectors);
class CookieMonster {
cache;
browser;
@@ -14,16 +23,21 @@ class CookieMonster {
constructor(cache) {
this.cache = cache;
createCacheDirectories(TEMP_DIR.screenshots);
}
async submitForm(plate) {
await this.page.focus(Selectors.form.plate);
await this.page.keyboard.type(plate);
const path = {
s: `j_idt${SUBMIT_FORM_ID}:j_idt137`,
u: `j_idt${SUBMIT_FORM_ID}`,
};
await this.page.evaluate(() => {
// eslint-disable-next-line no-undef
PrimeFaces.ab({
s: 'j_idt104:j_idt131',
u: 'j_idt104',
s: 'j_idt110:j_idt137',
u: 'j_idt110',
});
});
await this.page.waitForNavigation({
@@ -33,7 +47,7 @@ class CookieMonster {
}
async launchPage() {
this.browser = await puppeteer.launch();
this.browser = await puppeteer.launch(getPuppeteerOptions());
this.page = await this.browser.newPage();
await this.page.goto(SEARCH_URL);
await this.page.setCookie(...SITE_COOKIES);
@@ -48,14 +62,23 @@ class CookieMonster {
}
async init(plate) {
console.log(`Fetching data for ${plate}`);
await this.launchPage();
await this.submitForm(plate);
const pageContent = await this.page
.$eval(Selectors.container.main, (element) => element.innerHTML);
await this.cleanup(plate);
console.log(`Successfully fetched fresh data for ${plate}`);
return pageContent;
try {
console.log(`Fetching data for ${plate}`);
await this.launchPage();
try {
await this.submitForm(plate);
} catch (e) {
console.warn(`Got a timeout for ${plate} but data might still be there, sometimes takes longer to load entire page.`, e.message);
}
const pageContent = await this.page
.$eval(Selectors.container.main, (element) => element.innerHTML);
await this.cleanup(plate);
console.log(`Successfully fetched fresh data for ${plate}`);
return pageContent;
} catch (e) {
await this.cleanup(plate);
throw e;
}
}
}

View File

@@ -15,8 +15,8 @@ class PlateRecognizr {
fetch(image) {
const body = new FormData();
body.append('upload', fs.createReadStream('test.jpg'));
body.append('regions', 'gb'); // Change to your country
body.append('upload', image);
body.append('regions', 'ee'); // Change to your country
return fetch(ALPR_API_PATH, {
method: 'POST',
headers: {

View File

@@ -28,6 +28,7 @@ class Hack {
async init(plate) {
try {
const data = await this.getData(plate);
console.log(data);
this.scraper.setContent(data);
} catch (e) {
console.error(e);

View File

@@ -9,7 +9,7 @@ class Scraper {
setContent(text) {
const parsedContent = new JSDOM(text).window.document;
if (parsedContent.querySelector(Selectors.properties.main.container) === null) {
throw Error('No data was received.. Something went wrong.');
throw Error('No data or invalid data was received.. Something went wrong.');
}
this.document = parsedContent;
}

View File

@@ -12,7 +12,7 @@ class Main {
init() {
this.api.init();
this.alpr.handle();
// this.alpr.handle();
}
}

View File

@@ -2,21 +2,34 @@ import express from 'express';
import http from 'http';
import compression from 'compression';
import helmet from 'helmet';
import multer from 'multer';
import bodyParser from 'body-parser';
import Hack from '../components/Hack';
import PlateRecognizr from '../api/PlateRecognizr';
const PORT = process.env.PORT || "8000";
const PORT = process.env.PORT || '8000';
const upload = multer({ dest: '/tmp' });
class BasicApi {
app;
server;
hack;
alpr;
constructor() {
this.app = express();
this.app.use(express.static('public'));
this.app.use(bodyParser.urlencoded({ extended: false }));
this.app.use(compression());
this.app.use(helmet());
this.app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
next();
});
this.server = http.createServer(this.app);
this.hack = new Hack();
this.alpr = new PlateRecognizr();
}
init() {
@@ -34,6 +47,14 @@ class BasicApi {
.status(200)
.send(this.hack.getCar());
});
this.app.post('/alpr', upload.single('upload'), async ({ body }, response) => {
const { upload: image } = body;
console.log(Object.keys(body));
const plate = await (await this.alpr.fetch(image)).json();
console.log(plate);
response.status(200).json(plate);
});
}
}

View File

@@ -1,5 +1,6 @@
import Cacheman from 'cacheman';
import { CACHE } from './Constants';
import { createCacheDirectories } from './TempDirCreator';
const formatKey = (name) => {
if (!name) {
@@ -12,6 +13,7 @@ class Cache {
manager;
constructor() {
createCacheDirectories(CACHE.directory);
this.manager = new Cacheman({
ttl: CACHE.ttl,
engine: CACHE.engine,

View File

@@ -1,10 +1,13 @@
// seems that this changes sometimes
export const SUBMIT_FORM_ID = '110';
export default {
form: {
plate: '#j_idt104\\:regMark',
plate: `#j_idt${SUBMIT_FORM_ID}\\:regMark`,
},
container: {
main: '#content',
form: '#j_idt104',
form: `#j_idt${SUBMIT_FORM_ID}`,
},
properties: {
plate: '.content-title h1',

View File

@@ -0,0 +1,15 @@
import fs from 'fs';
/* eslint-disable import/prefer-default-export */
export const createCacheDirectories = async (targetDir) => {
if (!fs.existsSync(targetDir)) {
console.log(`Directory ${targetDir} does not exist. Creating it.`);
fs.mkdirSync(targetDir, {
recursive: true,
});
} else {
console.log(`Directory ${targetDir} already exists.`);
}
return true;
};

5283
api/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
const FtpDeploy = require("ftp-deploy");
const ftpDeploy = new FtpDeploy();
const ftp = new FtpDeploy();
const config = {
user: process.env.FTP_USER,
@@ -8,9 +9,7 @@ const config = {
port: process.env.FTP_PORT,
localRoot: __dirname + "/../frontend",
remoteRoot: "/public_html/car/",
// include: ["*", "**/*"], // this would upload everything except dot files
include: ["build/**"],
// e.g. exclude sourcemaps, and ALL files in node_modules (including dot files)
exclude: ["build/**/*.map", "node_modules/**", "node_modules/**/.*", ".git/**"],
// delete ALL existing files at destination before uploading, if true
deleteRemote: false,
@@ -18,9 +17,9 @@ const config = {
forcePasv: true
};
ftpDeploy
ftp
.deploy(config)
.then(res => {
.then(() => {
console.log("Deployment to FTP finished");
return;
})

View File

@@ -53,6 +53,7 @@ module.exports = (api) => {
'@context': './src/context',
'@components': './src/components',
'@hook': './src/hook',
'@actions': './src/actions',
'@assets': './assets',
}
}]

View File

@@ -125,5 +125,6 @@ module.exports = {
],
},
],
'no-console': 0,
},
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

14628
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,27 +1,19 @@
{
"name": "react-base",
"name": "car-fetcher-frontend",
"version": "0.0.1",
"description": "Base React template",
"description": "Frontend for car fetcher",
"main": "index.js",
"scripts": {
"start": "nodemon --exec \"cross-env NODE_ENV=development webpack-dev-server\"",
"start": "nodemon --exec \"cross-env NODE_ENV=development webpack-dev-server -r dotenv/config \"",
"prebundle": "rimraf build/*",
"bundle": "cross-env NODE_ENV=production webpack",
"cacheclean": "rimraf node_modules/.cache/hard-source",
"precommit": "lint-staged",
"lint": "eslint --fix src --ext .jsx"
},
"author": "k4rli",
"contributors": [],
"keywords": [
"react",
"reactjs",
"react-boilerplate"
],
"license": "MIT",
"engines": {
"node": ">=11",
"npm": ">=6"
"node": "^14",
"npm": "^6.14"
},
"lint-staged": {
"*.{js,jsx,css,md}": [
@@ -30,66 +22,70 @@
]
},
"dependencies": {
"@reduxjs/toolkit": "1.3.6",
"@fortawesome/fontawesome-free": "5.13.1",
"@material-ui/core": "4.11.0",
"@material-ui/icons": "4.9.1",
"@reduxjs/toolkit": "1.4.0",
"core-js": "3.6.5",
"react": "16.13.1",
"react-dom": "16.13.1",
"react-hot-loader": "4.12.21",
"react-webcam": "5.0.2",
"react-webcam": "5.2.0",
"regenerator-runtime": "0.13.5",
"use-dark-mode": "2.3.1"
},
"devDependencies": {
"@babel/core": "7.9.6",
"@babel/plugin-proposal-class-properties": "7.8.3",
"@babel/plugin-proposal-decorators": "7.8.3",
"@babel/plugin-proposal-export-namespace-from": "7.8.3",
"@babel/plugin-proposal-function-sent": "7.8.3",
"@babel/plugin-proposal-json-strings": "7.8.3",
"@babel/plugin-proposal-numeric-separator": "7.8.3",
"@babel/plugin-proposal-optional-chaining": "7.9.0",
"@babel/plugin-proposal-throw-expressions": "7.8.3",
"@babel/core": "7.10.4",
"@babel/plugin-proposal-class-properties": "7.10.4",
"@babel/plugin-proposal-decorators": "7.10.4",
"@babel/plugin-proposal-export-namespace-from": "7.10.4",
"@babel/plugin-proposal-function-sent": "7.10.4",
"@babel/plugin-proposal-json-strings": "7.10.4",
"@babel/plugin-proposal-numeric-separator": "7.10.4",
"@babel/plugin-proposal-optional-chaining": "7.10.4",
"@babel/plugin-proposal-throw-expressions": "7.10.4",
"@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/plugin-syntax-import-meta": "7.8.3",
"@babel/plugin-transform-runtime": "7.9.6",
"@babel/preset-env": "7.9.6",
"@babel/preset-react": "7.9.4",
"@babel/register": "7.9.0",
"@babel/runtime-corejs3": "7.9.6",
"@babel/plugin-syntax-import-meta": "7.10.4",
"@babel/plugin-transform-runtime": "7.10.4",
"@babel/preset-env": "7.10.4",
"@babel/preset-react": "7.10.4",
"@babel/register": "7.10.4",
"@babel/runtime-corejs3": "7.10.4",
"babel-eslint": "10.1.0",
"babel-loader": "8.1.0",
"babel-plugin-module-resolver": "4.0.0",
"browserslist": "4.12.0",
"browserslist": "4.13.0",
"connect-history-api-fallback": "1.6.0",
"copy-webpack-plugin": "6.0.1",
"copy-webpack-plugin": "6.0.3",
"cross-env": "7.0.2",
"css-loader": "3.5.3",
"eslint": "7.0.0",
"eslint-config-airbnb": "18.1.0",
"css-loader": "3.6.0",
"dotenv": "8.2.0",
"eslint": "7.4.0",
"eslint-config-airbnb": "18.2.0",
"eslint-import-resolver-babel-module": "5.1.2",
"eslint-loader": "4.0.2",
"eslint-plugin-import": "2.20.2",
"eslint-plugin-jsx-a11y": "6.2.3",
"eslint-plugin-react": "7.20.0",
"eslint-plugin-react-hooks": "4.0.2",
"eslint-plugin-import": "2.22.0",
"eslint-plugin-jsx-a11y": "6.3.1",
"eslint-plugin-react": "7.20.3",
"eslint-plugin-react-hooks": "4.0.6",
"file-loader": "6.0.0",
"hard-source-webpack-plugin": "0.13.1",
"html-webpack-plugin": "4.3.0",
"koa-connect": "2.0.1",
"lint-staged": "10.2.2",
"koa-connect": "2.1.0",
"lint-staged": "10.2.11",
"mini-css-extract-plugin": "0.9.0",
"node-sass": "4.14.1",
"nodemon": "2.0.4",
"optimize-css-assets-webpack-plugin": "5.0.3",
"rimraf": "3.0.2",
"sass-loader": "8.0.2",
"sass-loader": "9.0.2",
"script-ext-html-webpack-plugin": "2.1.4",
"style-loader": "1.2.1",
"terser-webpack-plugin": "3.0.1",
"terser-webpack-plugin": "3.0.6",
"webpack": "4.43.0",
"webpack-bundle-analyzer": "3.7.0",
"webpack-cli": "3.3.11",
"webpack-bundle-analyzer": "3.8.0",
"webpack-cli": "3.3.12",
"webpack-dev-server": "3.11.0",
"webpack-merge": "4.2.2"
"webpack-merge": "5.0.8"
}
}

File diff suppressed because one or more lines are too long

View File

@@ -5,7 +5,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>&#x1F615;</title>
<link type="text/css" rel="stylesheet" href="./assets/css/skeleton.min.css" />
<link type="text/css" rel="stylesheet" href="https://fonts.googleapis.com/css?family=Inconsolata&display=swap">
<link rel="apple-touch-icon" sizes="180x180" href="./assets/images/icon.png">
<link rel="icon" type="image/png" sizes="64x64" href="./assets/images/icon.png">
</head>

View File

@@ -0,0 +1,43 @@
import {
alprFetchSuccess, alprFetchStart, dataFetchStart, dataFetchSuccess,
} from '@slice/RecognitionSlice';
const API_BASE_PATH = process.env.API_BASE_PATH;
console.log(process.env.API_BASE_PATH);
export const getPlateRecognized = (image) => async (dispatch) => {
console.log(image);
dispatch(alprFetchStart());
const body = new FormData();
body.append('upload', image);
try {
console.log(API_BASE_PATH);
const response = await fetch(`${API_BASE_PATH}/alpr`, {
method: 'POST',
body,
});
console.log(response);
const data = await response.json();
dispatch(alprFetchSuccess(data));
console.log(data);
} catch (e) {
console.log(e);
}
};
export const fetchDataForPlate = (plate) => async (dispatch) => {
dispatch(dataFetchStart());
console.log('Fetching data for plate', plate);
try {
const response = await fetch(`${API_BASE_PATH}/${plate}`, {
method: 'GET',
});
console.log(response);
const data = await response.json();
dispatch(dataFetchSuccess(data));
console.log(data);
} catch (e) {
console.log(e);
}
};
export const getPlateData = (plate) => null;

View File

@@ -0,0 +1,25 @@
import React, { useContext } from 'react';
import RootContext from '@context/RootContext';
import { VIEW } from '@slice/StateSlice';
import { fetchDataForPlate } from '@actions/CaptureActions';
import Button from './common/Button';
const BottomNavigation = () => {
const { state: { state, alpr: { results } }, dispatch } = useContext(RootContext);
switch (state.view) {
case VIEW.alprResults:
return (
<>
<div className="row">
<Button text="Fetch data" onClick={() => fetchDataForPlate(results[0].plate)(dispatch)} />
</div>
</>
);
case VIEW.camera:
default:
return null;
}
};
export default BottomNavigation;

View File

@@ -0,0 +1,34 @@
import React, { useContext } from 'react';
import RootContext from '@context/RootContext';
const DataView = () => {
const { state, dispatch } = useContext(RootContext);
const { results, lastFetchedDataPlate } = state.alpr;
const { plate, score } = results;
return (
<div className="row">
<h2>{plate.toUpperCase()}</h2>
<h3>
Confidence
{' '}
{score}
</h3>
{lastFetchedDataPlate === plate && (
<ul>
{Object.entries(results).map(([key, value]) => (
<li key={`${key}`}>
<b>{key}</b>
{' '}
:
{' '}
{value}
</li>
))}
</ul>
)}
</div>
);
};
export default DataView;

View File

@@ -0,0 +1,35 @@
import React, { useContext } from 'react';
import RootContext from '@context/RootContext';
import Button from '@components/common/Button';
import { fetchDataForPlate } from '@actions/CaptureActions';
import DataView from './DataView';
const RecognitionResults = () => {
const { state, dispatch } = useContext(RootContext);
const { results, lastFetchedDataPlate } = state.alpr;
const fetchData = (plate) => plate && fetchDataForPlate(plate)(dispatch);
console.log(results);
const getPlate = () => {
if (!results || !results.plate) {
return (<h2>No plate detected</h2>);
}
return (
<>
<DataView />
<div className="row">
<Button text="Fetch data" onClick={() => fetchData(results.plate)} />
</div>
</>
);
};
return (
<div className="container">
{getPlate()}
</div>
);
};
export default RecognitionResults;

View File

@@ -0,0 +1,114 @@
import React, {
useState, useEffect, useRef, useCallback, useContext,
} from 'react';
import Webcam from 'react-webcam';
import FacingMode from '@constant/FacingMode';
import Loader from '@components/common/Loader';
import '@style/RootContainer.scss';
import RootContext from '@context/RootContext';
import { resetResults } from '@slice/RecognitionSlice';
import CameraOptions from './CameraOptions';
import Capture from './Capture';
const CAMERA_OPTIONS = {
format: 'image/jpeg',
minHeight: 1920,
minWidth: 1080,
quality: 0.75,
defaultView: FacingMode.FRONT,
};
const CameraContainer = () => {
const { dispatch } = useContext(RootContext);
const ref = useRef(null);
const [imgSrc, setImgSrc] = useState(null);
const [captureError, setCaptureError] = useState(null);
const [videoInputs, setVideoInputs] = useState([]);
const [resetCameraView, setResetCameraView] = useState(false);
const [videoConstraints, setVideoConstraints] = useState({
facingMode: CAMERA_OPTIONS.defaultView,
});
useEffect(() => {
const gotDevices = (mediaDevices) => new Promise((resolve, reject) => {
const availableVideoInputs = mediaDevices
.filter(({ kind }) => kind === 'videoinput')
.map(({ deviceId, label }) => ({ deviceId, label }));
if (availableVideoInputs.length > 0) {
resolve(availableVideoInputs);
} else {
reject(new Error('ERR::NO_MEDIA_TO_STREAM'));
}
});
navigator.mediaDevices
.enumerateDevices()
.then(gotDevices)
.then((availableVideoInputs) => setVideoInputs({ availableVideoInputs }))
.catch((err) => console.error(err));
}, []);
const changeCameraView = () => {
if (videoInputs.length === 1) {
console.error('ERR::AVAILABLE_MEDIA_STREAMS_IS_1');
return;
}
setResetCameraView(true);
setTimeout(() => {
const { facingMode } = videoConstraints;
const newFacingMode = facingMode === FacingMode.FRONT ? FacingMode.REAR : FacingMode.FRONT;
setResetCameraView(false);
setVideoConstraints({ ...videoConstraints, facingMode: newFacingMode });
}, 100);
};
const capture = useCallback(() => {
const imageSrc = ref.current.getScreenshot();
setImgSrc(imageSrc);
if (!imageSrc) {
setCaptureError(['Failed to capture camera. Please try again.']);
} else {
setCaptureError(null);
}
}, [ref, setImgSrc]);
const onBackClick = () => {
setImgSrc(null);
dispatch(resetResults());
};
const cameraView = () => (
<>
<div className="row">
{resetCameraView ? (
<Loader />
) : (
<Webcam
mirrored={false}
width="100%"
height="100%"
audio={false}
ref={ref}
screenshotFormat={CAMERA_OPTIONS.format}
minScreenshotWidth={CAMERA_OPTIONS.minWidth}
minScreenshotHeight={CAMERA_OPTIONS.minHeight}
screenshotQuality={CAMERA_OPTIONS.quality}
videoConstraints={videoConstraints} />
)}
</div>
<CameraOptions capture={capture} changeCameraView={changeCameraView} />
</>
);
return (
<>
{imgSrc
? <Capture imgSrc={imgSrc} error={captureError} onBackClick={onBackClick} />
: cameraView()}
</>
);
};
export default CameraContainer;

View File

@@ -0,0 +1,11 @@
import React from 'react';
import Button from '@components/common/Button';
const CameraOptions = ({ capture, changeCameraView }) => (
<div className="row button-container">
<Button text="Switch camera" onClick={changeCameraView} />
<Button text="Take image" onClick={capture} />
</div>
);
export default CameraOptions;

View File

@@ -0,0 +1,40 @@
import React, { useContext, useState } from 'react';
import Loader from '@components/common/Loader';
import { getPlateRecognized } from '@actions/CaptureActions';
import Button from '@components/common/Button';
import RootContext from '@context/RootContext';
import RecognitionResults from '@components/alpr/RecognitionResults';
const UploadCapture = ({ imgSrc, onBackClick }) => {
const [hasFetched, setHasFetched] = useState(true);
const { state: { alpr: { isFetchingALPR } }, dispatch } = useContext(RootContext);
const onUploadClick = () => {
getPlateRecognized(imgSrc)(dispatch);
setHasFetched(true);
};
return (
<div className="container">
{isFetchingALPR ? <Loader /> : (
<div className="row">
<img src={imgSrc} alt="captured" />
</div>
)}
{hasFetched && !isFetchingALPR && (<RecognitionResults />)}
<div className="row button-container">
<Button text="Back" onClick={onBackClick} />
{!hasFetched && <Button text="Upload" onClick={onUploadClick} />}
</div>
</div>
);
};
const Capture = ({
imgSrc, isCapturing, error, onBackClick,
}) => (
<div className="capture-container row">
{isCapturing && (<Loader />)}
{error ? (<p>{error}</p>) : (<UploadCapture imgSrc={imgSrc} onBackClick={onBackClick} />)}
</div>
);
export default Capture;

View File

@@ -0,0 +1,9 @@
import React from 'react';
const Button = ({ text, onClick }) => (
<button className="button light" type="button" onClick={onClick}>
{text}
</button>
);
export default Button;

View File

@@ -0,0 +1,6 @@
import React from 'react';
import './Loader.scss';
const Loader = () => (<div className="loader">Loading...</div>);
export default Loader;

View File

@@ -0,0 +1,46 @@
.loader,
.loader:after {
border-radius: 50%;
width: 10em;
height: 10em;
}
.loader {
margin: 60px auto;
font-size: 10px;
position: relative;
text-indent: -9999em;
border-top: 1.1em solid rgba(255, 255, 255, 0.2);
border-right: 1.1em solid rgba(255, 255, 255, 0.2);
border-bottom: 1.1em solid rgba(255, 255, 255, 0.2);
border-left: 1.1em solid #ffffff;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
-webkit-animation: load8 1.1s infinite linear;
animation: load8 1.1s infinite linear;
}
@-webkit-keyframes load8 {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes load8 {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}

View File

@@ -0,0 +1,6 @@
export default {
FRONT: 'user',
REAR: {
exact: 'environment',
},
};

View File

@@ -1,21 +1,13 @@
import React, { useReducer, useContext } from 'react';
import { usePersistedContext, usePersistedReducer } from '@hook/usePersist';
import RootContainer from '@container/RootContainer';
import RootContext from '@context/RootContext';
import RootReducer from '@slice/RootSlice';
import Properties from '@constant/Properties';
const App = () => {
const globalStore = usePersistedContext(
useContext(RootContext),
Properties.STORE_KEY,
);
const globalStore = useContext(RootContext);
const [state, dispatch] = usePersistedReducer(
useReducer(RootReducer, globalStore),
Properties.STORE_KEY,
);
const [state, dispatch] = useReducer(RootReducer, globalStore);
return (
<RootContext.Provider value={{ state, dispatch }}>

View File

@@ -1,91 +1,14 @@
import React, {
useState, useRef, useEffect, useCallback,
} from 'react';
import Webcam from 'react-webcam';
import React from 'react';
import CameraContainer from '@components/camera/CameraContainer';
import '@style/RootContainer.scss';
import BottomNavigation from '@components/BottomNavigation';
import '@style/RootContainer';
const RootContainer = () => {
const webcamRef = useRef(null);
const [videoInputs, setVideoInputs] = useState([]);
const [resetCameraView, setResetCameraView] = useState(false);
const [videoConstraints, setVideoConstraints] = useState({
facingMode: { exact: 'environment' },
});
const [imgSrc, setImgSrc] = useState(null);
const capture = useCallback(() => {
const imageSrc = webcamRef.current.getScreenshot();
setImgSrc(imageSrc);
}, [webcamRef, setImgSrc]);
useEffect(() => {
const gotDevices = (mediaDevices) => new Promise((resolve, reject) => {
const availableVideoInputs = [];
mediaDevices.forEach((mediaDevice) => {
if (mediaDevice.kind === 'videoinput') {
availableVideoInputs.push({
deviceId: mediaDevice.deviceId,
label: mediaDevice.label,
});
}
});
if (availableVideoInputs.length > 0) {
resolve(availableVideoInputs);
} else {
reject(new Error('ERR::NO_MEDIA_TO_STREAM'));
}
});
navigator.mediaDevices
.enumerateDevices()
.then(gotDevices)
.then((availableVideoInputs) => setVideoInputs({ availableVideoInputs }))
.catch((err) => console.error(err));
}, []);
const changeCameraView = () => {
if (videoInputs.length === 1) {
return console.error('ERR::AVAILABLE_MEDIA_STREAMS_IS_1');
}
setResetCameraView(true);
setTimeout(() => {
const { facingMode } = videoConstraints;
const newFacingMode = facingMode === 'user' ? { exact: 'environment' } : 'user';
setResetCameraView(false);
setVideoConstraints({ ...videoConstraints, facingMode: newFacingMode });
}, 100);
};
return (
<>
{resetCameraView ? (
'Loading...'
) : (
<Webcam
audio={false}
height="100%"
ref={webcamRef}
screenshotFormat="image/png"
minScreenshotWidth={1080}
minScreenshotHeight={1920}
screenshotQuality={1}
width="100%"
videoConstraints={videoConstraints} />
)}
<button type="button" onClick={changeCameraView}>
Switch camera
</button>
<button type="button" onClick={capture}>
Take image
</button>
{imgSrc && (<img src={imgSrc} />)}
</>
);
};
const RootContainer = () => (
<div className="container root-container">
<div className="header" />
<CameraContainer />
<BottomNavigation />
</div>
);
export default RootContainer;

View File

@@ -1,3 +1,8 @@
import { createContext } from 'react';
import { initialState as alpr } from '@slice/RecognitionSlice';
import { initialState as state } from '@slice/StateSlice';
export default createContext({}); // include initialStates here
export default createContext({
alpr,
state,
});

View File

@@ -1,23 +0,0 @@
import { useEffect, useRef } from 'react';
// https://overreacted.io/making-setinterval-declarative-with-react-hooks/
const useInterval = (callback, delay) => {
const savedCallback = useRef();
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
const tick = () => savedCallback.current();
if (delay !== null) {
const id = setInterval(tick, delay);
return () => clearInterval(id);
}
return () => {};
}, [delay]);
};
export default useInterval;

View File

@@ -1,16 +0,0 @@
import { useEffect } from 'react';
const DEFAULT_STATE_KEY = 'state';
export const usePersistedContext = (context, key = DEFAULT_STATE_KEY) => {
const persistedContext = localStorage.getItem(key);
return persistedContext ? JSON.parse(persistedContext) : context;
};
export const usePersistedReducer = ([state, dispatch], key = DEFAULT_STATE_KEY) => {
useEffect(
() => localStorage.setItem(key, JSON.stringify(state)),
[key, state],
);
return [state, dispatch];
};

View File

@@ -0,0 +1,66 @@
/* eslint-disable no-param-reassign */
import {
createSlice,
} from '@reduxjs/toolkit';
export const initialState = {
isFetchingALPR: false,
isFetchingData: false,
lastFetchedDataPlate: '540BLG',
results: {
plate: '540BLG',
name: 'ALFA ROMEO 159 SPORTWAGON',
vin: 'ZAR93900007055169',
'first registration': '07.09.2006',
category: 'passenger car',
body: 'universal',
'body colour': 'grey',
engine: '2387 cm',
'engine power': '147 kW',
fuel: 'Diesel',
transmission: 'Manual',
drivetrain: 'front wheel drive',
'registration certificate': 'EL573054',
score: 1.0,
},
};
const recognitionSlice = createSlice({
name: 'alpr',
initialState,
reducers: {
alprFetchStart(state) {
state.isFetchingALPR = true;
},
alprFetchSuccess(state, {
payload,
}) {
state.isFetchingALPR = false;
state.results = payload.results;
},
resetResults(state) {
state.results = [];
state.lastFetchedDataPlate = null;
},
dataFetchStart(state) {
state.isFetchingData = true;
},
dataFetchSuccess(state, {
payload,
}) {
state.isFetchingData = false;
state.results = { ...state.results, ...payload };
state.lastFetchedDataPlate = payload ? payload.plate : null;
},
},
});
export const {
alprFetchStart,
alprFetchSuccess,
resetResults,
dataFetchStart,
dataFetchSuccess,
} = recognitionSlice.actions;
export default recognitionSlice.reducer;

View File

@@ -1,3 +1,8 @@
import { combineReducers } from '@reduxjs/toolkit';
import alpr from '@slice/RecognitionSlice';
import state from '@slice/StateSlice';
export default combineReducers({}); // include slices here
export default combineReducers({
alpr,
state,
});

View File

@@ -0,0 +1,29 @@
/* eslint-disable no-param-reassign */
import { createSlice } from '@reduxjs/toolkit';
export const VIEW = {
camera: 'camera',
capture: 'capture',
alprResults: 'alprResults',
dataResults: 'dataResults',
};
export const initialState = {
view: VIEW.camera,
};
const stateSlice = createSlice({
name: 'state',
initialState,
reducers: {
setView(state, { payload }) {
state.view = payload;
},
},
});
export const {
setView,
} = stateSlice.actions;
export default stateSlice.reducer;

View File

@@ -1,10 +1,65 @@
@import 'palette.scss';
@import 'constants.scss';
@import './common/font/camphor.scss';
@import './common/palette.scss';
@import './common/constants.scss';
body {
margin: 0;
font-family: 'Roboto';
font-family: 'Camphor' !important;
font: 400 13.333px 'Camphor' !important;
overflow-x: hidden;
width: 100vw;
height: 100vh;
background-color: $orange;
}
.root-container {
//box-shadow: 0 30px 40px rgba(0,0,0,.1);
padding: 2em;
margin-top: 2em;
height: 90vh;
display: flex;
flex-direction: column;
.header {
background-image: url('../../assets/images/Maanteeamet.png');
background-size: cover;
height: 120px;
margin-bottom: 3em;
background-repeat: no-repeat;
background-position: center;
}
img {
text-align: center;
}
}
.button-container {
margin-top: 1em;
text-align: center;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
button {
margin: 0.25em;
font-family: inherit;
&.light {
background-color: rgba(255, 121, 121, 0.5);
border: 1px solid rgba(255, 121, 121, 1.0);
}
}
}
.capture-container {
canvas {
max-width: 100%;
width: 100%;
}
& img {
max-width: 100%;
max-height: auto;
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
$orange: #fed8b1;

View File

@@ -1,6 +1,6 @@
/* eslint-disable import/no-extraneous-dependencies */
const { ProgressPlugin } = require('webpack');
const { ProgressPlugin, EnvironmentPlugin } = require('webpack');
const convert = require('koa-connect');
const history = require('connect-history-api-fallback');
const HtmlWebpackPlugin = require('html-webpack-plugin');
@@ -71,6 +71,7 @@ module.exports = {
extensions: ['*', '.js', '.jsx', '.css', '.scss'],
},
plugins: [
new EnvironmentPlugin(['NODE_ENV', 'API_BASE_PATH']),
new ProgressPlugin(),
new HtmlWebpackPlugin({
template: commonPaths.templatePath,

View File

@@ -46,44 +46,44 @@ module.exports = {
contentBase: commonPaths.outputPath,
compress: true,
hot: true,
port: 3001,
port: 3333,
},
plugins: [
new HotModuleReplacementPlugin(),
new HardSourceWebpackPlugin({
// Either an absolute path or relative to webpack's options.context.
cacheDirectory: commonPaths.cachePath,
// Either a string of object hash function given a webpack config.
// node-object-hash on npm can be used to build this.
configHash: (webpackConfig) => require('node-object-hash')({ // eslint-disable-line
sort: false,
}).hash(webpackConfig),
// Either false, a string, an object, or a project hashing function.
environmentHash: {
root: process.cwd(),
directories: [],
files: [
// Cache will get an unique hash based on those files
// if either of them changes, new cache must be generated
'package-lock.json',
'.babelrc.js',
],
},
info: {
mode: 'none',
level: 'debug',
},
// Clean up large, old caches automatically.
cachePrune: {
// Caches younger than `maxAge` are not considered for deletion.
// They must be at least this old in milliseconds.
maxAge: 3 * 60 * 60 * 1000, // 3 hours
// All caches together must be larger than `sizeThreshold` before any
// caches will be deleted.
// Together they must be at least this big in bytes.
sizeThreshold: 50 * 1024 * 1024, // 50 MB
},
}),
// new HardSourceWebpackPlugin({
// // Either an absolute path or relative to webpack's options.context.
// cacheDirectory: commonPaths.cachePath,
// // Either a string of object hash function given a webpack config.
// // node-object-hash on npm can be used to build this.
// configHash: (webpackConfig) => require('node-object-hash')({ // eslint-disable-line
// sort: false,
// }).hash(webpackConfig),
// // Either false, a string, an object, or a project hashing function.
// environmentHash: {
// root: process.cwd(),
// directories: [],
// files: [
// // Cache will get an unique hash based on those files
// // if either of them changes, new cache must be generated
// 'package-lock.json',
// '.babelrc.js',
// ],
// },
// info: {
// mode: 'none',
// level: 'debug',
// },
// // Clean up large, old caches automatically.
// cachePrune: {
// // Caches younger than `maxAge` are not considered for deletion.
// // They must be at least this old in milliseconds.
// maxAge: 3 * 60 * 60 * 1000, // 3 hours
// // All caches together must be larger than `sizeThreshold` before any
// // caches will be deleted.
// // Together they must be at least this big in bytes.
// sizeThreshold: 50 * 1024 * 1024, // 50 MB
// },
// }),
new BundleAnalyzerPlugin({
analyzerPort: 8888,
openAnalyzer: false,

File diff suppressed because it is too large Load Diff

2
package-lock.json generated
View File

@@ -1,5 +1,5 @@
{
"name": "maanteeamet-fetch",
"name": "car-fetcher",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,

View File

@@ -1,10 +1,9 @@
{
"name": "maanteeamet-fetch",
"name": "car-fetcher",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "node api/src/index.js",
"heroku": "git subtree push --prefix api heroku master",
"predeploy": "cd frontend && npm run bundle",
"deploy": "node -r dotenv/config deploy/ftp.js"
},
"devDependencies": {