├── mockup.jpg
├── server.js
├── package.json
├── index.html
├── main.css
├── sharenow.js
├── Readme.md
└── main.js
/mockup.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/T3rm1/share-now-api/HEAD/mockup.jpg
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | const shareNow = require("./sharenow.js");
2 | const express = require("express");
3 | const WebSocket = require("ws");
4 |
5 | const app = express();
6 | const wss = new WebSocket.Server({port: 8081});
7 | let client = new shareNow();
8 | client.connect();
9 | wss.on("connection", ws => {
10 | ws.send(JSON.stringify(client.getVehicles(vehicleUpdate => {
11 | ws.send(JSON.stringify(vehicleUpdate));
12 | })))
13 | });
14 |
15 | app.use(express.static(__dirname));
16 | app.get("/", (req, res) => {
17 | res.sendFile("index.html", {root: __dirname});
18 | });
19 |
20 | app.listen(8080, () => {
21 | console.log("Server is up and running on port 8080.");
22 | });
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "share-now-api",
3 | "version": "1.0.0",
4 | "description": "Demo project to show how to use the Share Now api anonymously.",
5 | "main": "server.js",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/T3rm1/share-now-api.git"
9 | },
10 | "dependencies": {
11 | "express": "^4.17.1",
12 | "mqtt": "^4.0.1",
13 | "uuid-random": "^1.3.0",
14 | "ws": "^7.3.0"
15 | },
16 | "devDependencies": {},
17 | "scripts": {
18 | "test": "echo \"Error: no test specified\" && exit 1"
19 | },
20 | "keywords": [
21 | "mqtt",
22 | "sharenow"
23 | ],
24 | "author": "T3rm1",
25 | "license": "ISC",
26 | "homepage": "https://github.com/T3rm1/share-now-api"
27 | }
28 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Share Now cars
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/main.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | height: 100%;
3 | margin: 0px;
4 | font-family: Verdana, Geneva, Tahoma, sans-serif;
5 | }
6 |
7 | #map {
8 | height: 100%;
9 | }
10 |
11 | nav {
12 | padding: 10px;
13 | background-color: #0ba1e2;
14 | }
15 |
16 | nav ul {
17 | margin: 0px;
18 | display: flex;
19 | justify-content: center;
20 | align-content: center;
21 | list-style: none;
22 | margin: 10px;
23 | }
24 |
25 | nav ul li {
26 | margin-left: 10px;
27 | float: left;
28 | border-radius: 20px;
29 | color: white;
30 | padding: 10px;
31 | cursor: pointer;
32 | }
33 |
34 | nav ul li:hover:not(.selected) {
35 | background-color: rgba(255, 255, 255, 0.3);
36 | }
37 |
38 | li.selected {
39 | border: 2px solid white;
40 | }
41 |
42 | .marker {
43 | width: 2rem;
44 | height: 2rem;
45 | display: block;
46 | left: -1.0rem;
47 | top: -1.0rem;
48 | position: relative;
49 | border-radius: 3rem 3rem 0;
50 | transform: rotate(45deg);
51 | }
52 | .available {
53 | background-color: #0ba1e2;
54 | border: 1px solid #0ba1e2;
55 | }
56 | .unavailable {
57 | background-color: #53595c;
58 | border: 1px solid #53595c;
59 | }
60 | .new {
61 | background-color: #25a133;
62 | border: 1px solid #25a133;
63 | }
64 | .marker i {
65 | transform: rotate(-45deg);
66 | position: relative;
67 | top: 4px;
68 | left: 4px;
69 | font-size: 1.5rem;
70 | color: white;
71 | }
72 | .popup-car {
73 | display: flex;
74 | }
75 | .popup-car img {
76 | object-fit: contain;
77 | }
78 | .popup-car-details {
79 | justify-content: center;
80 | font-size: large;
81 | display: flex;
82 | flex-direction: column;
83 | }
--------------------------------------------------------------------------------
/sharenow.js:
--------------------------------------------------------------------------------
1 | const mqtt = require("mqtt");
2 | const zlip = require("zlib");
3 | const uuid = require("uuid-random");
4 |
5 | class ShareNowClient {
6 | static VEHICLELISTDELTA = "C2G/S2C/3/VEHICLELISTDELTA.GZ";
7 | static VEHICLELIST = "C2G/S2C/3/VEHICLELIST.GZ"
8 | vehicles = [];
9 | #updateCallback;
10 |
11 | connect() {
12 | let clientId = `a:${uuid()}`;
13 | let client = mqtt.connect('mqtts://driver.eu.share-now.com:443', {
14 | clientId,
15 | rejectUnauthorized: false,
16 | reconnectPeriod: 0
17 | });
18 |
19 | client.on('connect', () => {
20 | console.log("Connected to MQTT broker. Subscribing to topics.");
21 | client.subscribe(ShareNowClient.VEHICLELIST, {qos: 0});
22 | client.subscribe(ShareNowClient.VEHICLELISTDELTA, {qos: 1});
23 | });
24 |
25 | client.on("message", (topic, message) => {
26 | let json = JSON.parse(zlip.gunzipSync(message));
27 | if (topic === ShareNowClient.VEHICLELISTDELTA) {
28 | this.updateVehicles(json);
29 | if (this.#updateCallback !== undefined) {
30 | this.#updateCallback(json);
31 | }
32 | } else if (topic === ShareNowClient.VEHICLELIST) {
33 | console.log("Received initial vehicle list");
34 | client.unsubscribe(ShareNowClient.VEHICLELIST);
35 | this.vehicles = json.connectedVehicles;
36 | }
37 | });
38 |
39 | client.on("error", error => {
40 | console.log(`Error: ${error}`);
41 | });
42 |
43 | client.on("close", () => {
44 | console.log("Close");
45 | });
46 | }
47 |
48 | getVehicles(callback) {
49 | this.#updateCallback = callback;
50 | return this.vehicles;
51 | }
52 |
53 | updateVehicles(vehicleUpdate) {
54 | this.vehicles = this.vehicles.concat(vehicleUpdate.addedVehicles);
55 | vehicleUpdate.removedVehicles.forEach(vehicleId => {
56 | this.vehicles = this.vehicles.filter(e => e.id !== vehicleId);
57 | });
58 | }
59 | }
60 |
61 | module.exports = ShareNowClient;
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # Share Now car locations
4 | Here is [demo page](https://share-now-api.herokuapp.com) that shows the locations of all Share Now cars. Only cars in Hamburg are shown right now.
5 |
6 | Green markers are cars that became available.
7 |
8 | Grey markers are cars that became unavailable.
9 |
10 | ## Usage
11 | ```
12 | npm install
13 | node server.js
14 | ```
15 | Go to [http://localhost:8080](http://localhost:8080).
16 |
17 |
18 | ## MQTT
19 | The data is serverd by a MQTT broker. For Europe the endpoint is `mqtts://driver.eu.share-now.com:443`. The clientId is a random UUID prefixed with `a:` for anonymous connections. No username and password are required.
20 |
21 | You can subscribe to multiple topics. The schema is `C2G/S2C//.GZ` where `topic` is either `VEHICLELIST` or `VEHICLELISTDELTA`.
22 |
23 | Possible `locationId` values are:
24 |
25 | - Germany
26 | - Hamburg - `3`
27 | - Berlin - `12`
28 | - Frankfurt am Main - `33`
29 | - München - `26`
30 | - Köln - `19`
31 | - Stuttgart - `18`
32 | - Italy
33 | - Milan - `20`
34 | - Rome - `31`
35 | - Turin - `44`
36 | - Other countries
37 | - Copenhagen - `52`
38 | - Paris - `48`
39 | - Amsterdam - `5`
40 | - Vienna - `7`
41 | - Madrid - `36`
42 | - Budapest - `55`
43 |
44 | The data received is json compressed with gzip.
45 |
46 | ### VEHICLELIST
47 | Subscriptions to this topic will give you a list with all cars for the given location. A message is sent each time there is an update, so you should unsubscribe as soon as you get the first message.
48 |
49 |
50 | Example
51 |
52 | ```json
53 | {
54 | "connectedVehicles": [
55 | {
56 | "id": "WBY8P210107E82494",
57 | "plate": "M-EV1558E",
58 | "geoCoordinate": {
59 | "latitude": 53.57132,
60 | "longitude": 9.95367
61 | },
62 | "fuellevel": 84,
63 | "address": "Fruchtallee 107, 20259 Hamburg",
64 | "locationId": "3",
65 | "buildSeries": "BMW_I3",
66 | "fuelType": "ELECTRIC",
67 | "primaryColor": "B85U",
68 | "hardwareVersion": "HW42",
69 | "imageUrl": "https://www.car2go.com/rentalassets/vehicles/{density}/bmw_i3_capparis_white.png",
70 | "transmission": "GA",
71 | "rank": 1,
72 | "vin": "WBY8P210107E82494",
73 | "locationIdAsLong": 3
74 | }
75 | ],
76 | "locationId": 3,
77 | "eventType": "CONNECTED_VEHICLES",
78 | "timestamp": 1589666154706
79 | }
80 | ```
81 |
82 |
83 | ### VEHICLELISTDELTA
84 | This topic receives messages whenever a car becomes available or unavailable.
85 |
86 |
87 | Example
88 |
89 | ```json
90 | {
91 | "addedVehicles": [
92 | {
93 | "id": "WME4533421K323858",
94 | "plate": "HH-GO8560",
95 | "geoCoordinate": {
96 | "latitude": 53.55533,
97 | "longitude": 10.02782
98 | },
99 | "fuellevel": 59,
100 | "address": "Jungestra\u00c3\u0178e 6, 20535 Hamburg",
101 | "locationId": "3",
102 | "buildSeries": "C453",
103 | "fuelType": "GASOLINE",
104 | "primaryColor": "EN2U",
105 | "secondaryColor": "EDAO",
106 | "hardwareVersion": "HW3",
107 | "imageUrl": "https://www.car2go.com/rentalassets/vehicles/{density}/c453_silver.png",
108 | "transmission": "GA",
109 | "rank": 1,
110 | "vin": "WME4533421K323858",
111 | "locationIdAsLong": 3
112 | }
113 | ],
114 | "removedVehicles": [
115 | "WME4533421K291769"
116 | ],
117 | "locationId": 3,
118 | "timestamp": 1589656739007,
119 | "eventType": "VEHICLE_LIST_UPDATE"
120 | }
121 | ```
122 |
123 |
124 | ### Code
125 | All Share Now api related code is in [sharenow.js](sharenow.js). Rest of the files are for the demo webpage.
126 |
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 | const locations = {
2 | "Germany": [
3 | {name : "Hamburg", id: 3, geo: {lat: 53.57132, lng: 9.95367}},
4 | {name : "Berlin", id: 12, geo: {lat: 52.5069704, lng: 13.2846517}},
5 | {name : "Frankfurt am Main", id: 33, geo: {lat: 50.121301, lng: 8.5665248}},
6 | {name : "München", id: 26, geo: {lat: 48.155004, lng: 11.4717967}},
7 | {name : "Köln", id: 19, geo: {lat: 50.95779, lng: 6.8972834}},
8 | {name : "Stuttgart", id: 18, geo: {lat: 48.779301, lng: 9.1071762}}
9 | ],
10 | "Denmark": [
11 | {name : "Kopenhagen", id: 52, geo: {lat: 55.6713108, lng: 12.5588047}}
12 | ],
13 | "France": [
14 | {name : "Paris", id: 48, geo: {lat: 48.8589101, lng: 2.3120407}}
15 | ],
16 | "Italy": [
17 | {name : "Mailand", id: 20, geo: {lat: 45.4627887, lng: 9.142713}},
18 | {name : "Rom", id: 31, geo: {lat: 41.9101776, lng: 12.4659587}},
19 | {name : "Turin", id: 44, geo: {lat: 45.073544, lng: 7.6405873}}
20 | ],
21 | "Netherlands": [
22 | {name : "Amsterdam", id: 5, geo: {lat: 52.3547498, lng: 4.8339214}}
23 | ],
24 | "Austria": [
25 | {name : "Wien", id: 7, geo: {lat: 48.220778, lng: 16.3100209}}
26 | ],
27 | "Spain": [
28 | {name : "Madrid", id: 36, geo: {lat: 40.4380638, lng: -3.7495758}}
29 | ],
30 | "Hungary": [
31 | {name : "Budapest", id: 55, geo: {lat: 47.4813081, lng: 19.0602639}}
32 | ]
33 | }
34 | const idToCity = {};
35 | Object.keys(locations).forEach(country => locations[country].forEach(city => idToCity[city.id] = city));
36 |
37 | document.querySelectorAll("#countries li").forEach(li => {
38 | li.onclick = () => {
39 | updateCities(li.innerText);
40 | document.querySelectorAll("#countries li.selected").forEach(li => li.classList.remove("selected"));
41 | li.classList.add("selected");
42 | }
43 | });
44 | registerCityClickListeners();
45 | function registerCityClickListeners() {
46 | document.querySelectorAll("#cities li").forEach(li => {
47 | li.onclick = () => {
48 | changeLocation(li.dataset.id);
49 | }
50 | });
51 | }
52 |
53 | function updateCities(country) {
54 | ul = document.getElementById("cities");
55 | ul.innerHTML = "";
56 | locations[country].forEach(city => {
57 | ul.insertAdjacentHTML("beforeend", `${city.name}`)
58 | });
59 | registerCityClickListeners();
60 | }
61 | function changeLocation(id) {
62 | let city = idToCity[id];
63 | map.setView([city.geo.lat, city.geo.lng], 13);
64 | }
65 |
66 | var map = L.map('map').setView([53.57132, 9.95367], 13);
67 | L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
68 | attribution: '© OpenStreetMap contributors'
69 | }).addTo(map);
70 |
71 | let markers = {};
72 | function addVehicleMarker(vehicle, initial = false) {
73 | let geoCoords = vehicle.geoCoordinate;
74 | if (geoCoords !== undefined) {
75 | const icon = L.divIcon({
76 | iconAnchor: [0, 24],
77 | labelAnchor: [-6, 0],
78 | popupAnchor: [0, -36],
79 | html: ``
80 | });
81 | let fuelIcon = vehicle.fuelType === "ELECTRIC" ? '' : '';
82 | let marker = L.marker([vehicle.geoCoordinate.latitude, vehicle.geoCoordinate.longitude])
83 | .addTo(map)
84 | .setIcon(icon)
85 | .bindPopup(`
86 |
99 | `);
100 | markers[vehicle.id] = marker;
101 | }
102 | }
103 |
104 | function removeVehicleMarker(id) {
105 | let marker = markers[id];
106 | if (marker !== undefined) {
107 | //marker.removeFrom(map);
108 | //delete markers.id;
109 | const icon = L.divIcon({
110 | iconAnchor: [0, 24],
111 | labelAnchor: [-6, 0],
112 | popupAnchor: [0, -36],
113 | html: ``
114 | });
115 | marker.setIcon(icon);
116 | }
117 | }
118 |
119 | ws = new WebSocket("ws://localhost:8081");
120 | ws.onmessage = event => {
121 | json = JSON.parse(event.data);
122 | if (Array.isArray(json)) {
123 | // initial list
124 | json.forEach(v => addVehicleMarker(v, true));
125 | } else if ("VEHICLE_LIST_UPDATE" === json.eventType) {
126 | json.addedVehicles.forEach(addVehicleMarker);
127 | json.removedVehicles.forEach(removeVehicleMarker);
128 | }
129 | };
130 |
--------------------------------------------------------------------------------