diff --git a/Dockerfile b/Dockerfile
index 3ef213587d0dd9a7c3f63877f7eb5450c96b889e..88b5046798182ce34716adc7cd5cedc75075984c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -10,5 +10,6 @@ RUN npm run build
 # production environment
 FROM nginx:1.16.0-alpine
 COPY --from=build /usr/src/app/build /usr/share/nginx/html
+COPY default.conf /etc/nginx/conf.d/default.conf
 EXPOSE 80
 CMD ["nginx", "-g", "daemon off;"]
diff --git a/default.conf b/default.conf
new file mode 100644
index 0000000000000000000000000000000000000000..6f9b9322e0b2a4e07d2887abaf8ff13f4ae16c8c
--- /dev/null
+++ b/default.conf
@@ -0,0 +1,10 @@
+server {
+    listen       80;
+    server_name  localhost;
+    location / {
+        try_files $uri /index.html;
+        root   /usr/share/nginx/html;
+        index  index.html index.htm;
+    }
+    error_page   500 502 503 504  /50x.html;
+}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index a5d09a513ce24f8fbb0dba8bc7923b341a5fc223..6463ada39fd86f4a088ae8726c205a054b7c90cf 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1624,6 +1624,12 @@
       "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.0.tgz",
       "integrity": "sha512-sY5AXXVZv4Y1VACTtR11UJCPHHudgY5i26Qj5TypE6DKlIApbwb5uqhXcJ5UUGbvZNRh7EeIoW+LrJumBsKp7w=="
     },
+    "abbrev": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+      "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
+      "optional": true
+    },
     "accepts": {
       "version": "1.3.7",
       "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
@@ -1740,6 +1746,16 @@
       "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
       "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw=="
     },
+    "are-we-there-yet": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz",
+      "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
+      "optional": true,
+      "requires": {
+        "delegates": "^1.0.0",
+        "readable-stream": "^2.0.6"
+      }
+    },
     "argparse": {
       "version": "1.0.10",
       "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
@@ -2756,6 +2772,17 @@
       "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000971.tgz",
       "integrity": "sha512-TQFYFhRS0O5rdsmSbF1Wn+16latXYsQJat66f7S7lizXW1PVpWJeZw9wqqVLIjuxDRz7s7xRUj13QCfd8hKn6g=="
     },
+    "canvas": {
+      "version": "2.6.0",
+      "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.6.0.tgz",
+      "integrity": "sha512-bEO9f1ThmbknLPxCa8Es7obPlN9W3stB1bo7njlhOFKIdUTldeTqXCh9YclCPAi2pSQs84XA0jq/QEZXSzgyMw==",
+      "optional": true,
+      "requires": {
+        "nan": "^2.14.0",
+        "node-pre-gyp": "^0.11.0",
+        "simple-get": "^3.0.3"
+      }
+    },
     "capture-exit": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz",
@@ -3573,6 +3600,12 @@
         "date-now": "^0.1.4"
       }
     },
+    "console-control-strings": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
+      "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
+      "optional": true
+    },
     "constants-browserify": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz",
@@ -4048,11 +4081,26 @@
       "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
       "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU="
     },
+    "decompress-response": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz",
+      "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=",
+      "optional": true,
+      "requires": {
+        "mimic-response": "^1.0.0"
+      }
+    },
     "deep-equal": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz",
       "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU="
     },
+    "deep-extend": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+      "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
+      "optional": true
+    },
     "deep-is": {
       "version": "0.1.3",
       "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
@@ -4156,6 +4204,12 @@
       "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
       "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
     },
+    "delegates": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
+      "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
+      "optional": true
+    },
     "depd": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
@@ -4175,6 +4229,12 @@
       "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
       "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
     },
+    "detect-libc": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
+      "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=",
+      "optional": true
+    },
     "detect-newline": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz",
@@ -5292,6 +5352,82 @@
       "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
       "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU="
     },
+    "fabric": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/fabric/-/fabric-3.3.2.tgz",
+      "integrity": "sha512-xUbSmv3KzmmXgSTqHZbg33YMiIm3ZGlWvDgqEqdTBMSrrQd8V9t596ra7lIgr8rNh3HhxBDu7rZJ+R0t6FNFAg==",
+      "requires": {
+        "canvas": "^2.6.0",
+        "jsdom": "^15.1.0"
+      },
+      "dependencies": {
+        "jsdom": {
+          "version": "15.1.1",
+          "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-15.1.1.tgz",
+          "integrity": "sha512-cQZRBB33arrDAeCrAEWn1U3SvrvC8XysBua9Oqg1yWrsY/gYcusloJC3RZJXuY5eehSCmws8f2YeliCqGSkrtQ==",
+          "optional": true,
+          "requires": {
+            "abab": "^2.0.0",
+            "acorn": "^6.1.1",
+            "acorn-globals": "^4.3.2",
+            "array-equal": "^1.0.0",
+            "cssom": "^0.3.6",
+            "cssstyle": "^1.2.2",
+            "data-urls": "^1.1.0",
+            "domexception": "^1.0.1",
+            "escodegen": "^1.11.1",
+            "html-encoding-sniffer": "^1.0.2",
+            "nwsapi": "^2.1.4",
+            "parse5": "5.1.0",
+            "pn": "^1.1.0",
+            "request": "^2.88.0",
+            "request-promise-native": "^1.0.7",
+            "saxes": "^3.1.9",
+            "symbol-tree": "^3.2.2",
+            "tough-cookie": "^3.0.1",
+            "w3c-hr-time": "^1.0.1",
+            "w3c-xmlserializer": "^1.1.2",
+            "webidl-conversions": "^4.0.2",
+            "whatwg-encoding": "^1.0.5",
+            "whatwg-mimetype": "^2.3.0",
+            "whatwg-url": "^7.0.0",
+            "ws": "^7.0.0",
+            "xml-name-validator": "^3.0.0"
+          }
+        },
+        "tough-cookie": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz",
+          "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==",
+          "optional": true,
+          "requires": {
+            "ip-regex": "^2.1.0",
+            "psl": "^1.1.28",
+            "punycode": "^2.1.1"
+          }
+        },
+        "whatwg-url": {
+          "version": "7.0.0",
+          "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.0.0.tgz",
+          "integrity": "sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ==",
+          "optional": true,
+          "requires": {
+            "lodash.sortby": "^4.7.0",
+            "tr46": "^1.0.1",
+            "webidl-conversions": "^4.0.2"
+          }
+        },
+        "ws": {
+          "version": "7.1.0",
+          "resolved": "https://registry.npmjs.org/ws/-/ws-7.1.0.tgz",
+          "integrity": "sha512-Swie2C4fs7CkwlHu1glMePLYJJsWjzhl1vm3ZaLplD0h7OMkZyZ6kLTB/OagiU923bZrPFXuDTeEqaEN4NWG4g==",
+          "optional": true,
+          "requires": {
+            "async-limiter": "^1.0.0"
+          }
+        }
+      }
+    },
     "fast-deep-equal": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
@@ -5573,6 +5709,15 @@
         "universalify": "^0.1.0"
       }
     },
+    "fs-minipass": {
+      "version": "1.2.6",
+      "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.6.tgz",
+      "integrity": "sha512-crhvyXcMejjv3Z5d2Fa9sf5xLYVCF5O1c71QxbVnbLsmYMBEvDAftewesN/HhY03YRoA7zOMxjNGrF5svGaaeQ==",
+      "optional": true,
+      "requires": {
+        "minipass": "^2.2.1"
+      }
+    },
     "fs-write-stream-atomic": {
       "version": "1.0.10",
       "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz",
@@ -5605,6 +5750,59 @@
       "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
       "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc="
     },
+    "gauge": {
+      "version": "2.7.4",
+      "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
+      "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
+      "optional": true,
+      "requires": {
+        "aproba": "^1.0.3",
+        "console-control-strings": "^1.0.0",
+        "has-unicode": "^2.0.0",
+        "object-assign": "^4.1.0",
+        "signal-exit": "^3.0.0",
+        "string-width": "^1.0.1",
+        "strip-ansi": "^3.0.1",
+        "wide-align": "^1.1.0"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+          "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+          "optional": true
+        },
+        "is-fullwidth-code-point": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+          "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+          "optional": true,
+          "requires": {
+            "number-is-nan": "^1.0.0"
+          }
+        },
+        "string-width": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+          "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+          "optional": true,
+          "requires": {
+            "code-point-at": "^1.0.0",
+            "is-fullwidth-code-point": "^1.0.0",
+            "strip-ansi": "^3.0.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+          "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+          "optional": true,
+          "requires": {
+            "ansi-regex": "^2.0.0"
+          }
+        }
+      }
+    },
     "get-caller-file": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz",
@@ -5848,6 +6046,12 @@
       "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz",
       "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q="
     },
+    "has-unicode": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
+      "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=",
+      "optional": true
+    },
     "has-value": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz",
@@ -6174,6 +6378,15 @@
       "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
       "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg=="
     },
+    "ignore-walk": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz",
+      "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==",
+      "optional": true,
+      "requires": {
+        "minimatch": "^3.0.4"
+      }
+    },
     "immer": {
       "version": "1.10.0",
       "resolved": "https://registry.npmjs.org/immer/-/immer-1.10.0.tgz",
@@ -8275,6 +8488,12 @@
       "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
       "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ=="
     },
+    "mimic-response": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
+      "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
+      "optional": true
+    },
     "mini-create-react-context": {
       "version": "0.3.2",
       "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.3.2.tgz",
@@ -8318,6 +8537,25 @@
       "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
       "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
     },
+    "minipass": {
+      "version": "2.3.5",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz",
+      "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==",
+      "optional": true,
+      "requires": {
+        "safe-buffer": "^5.1.2",
+        "yallist": "^3.0.0"
+      }
+    },
+    "minizlib": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz",
+      "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==",
+      "optional": true,
+      "requires": {
+        "minipass": "^2.2.1"
+      }
+    },
     "mississippi": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz",
@@ -8458,6 +8696,28 @@
       "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
       "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc="
     },
+    "needle": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/needle/-/needle-2.4.0.tgz",
+      "integrity": "sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==",
+      "optional": true,
+      "requires": {
+        "debug": "^3.2.6",
+        "iconv-lite": "^0.4.4",
+        "sax": "^1.2.4"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.2.6",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+          "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+          "optional": true,
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        }
+      }
+    },
     "negotiator": {
       "version": "0.6.2",
       "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
@@ -8552,6 +8812,32 @@
         }
       }
     },
+    "node-pre-gyp": {
+      "version": "0.11.0",
+      "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz",
+      "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==",
+      "optional": true,
+      "requires": {
+        "detect-libc": "^1.0.2",
+        "mkdirp": "^0.5.1",
+        "needle": "^2.2.1",
+        "nopt": "^4.0.1",
+        "npm-packlist": "^1.1.6",
+        "npmlog": "^4.0.2",
+        "rc": "^1.2.7",
+        "rimraf": "^2.6.1",
+        "semver": "^5.3.0",
+        "tar": "^4"
+      },
+      "dependencies": {
+        "semver": {
+          "version": "5.7.0",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz",
+          "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==",
+          "optional": true
+        }
+      }
+    },
     "node-releases": {
       "version": "1.1.21",
       "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.21.tgz",
@@ -8567,6 +8853,16 @@
         }
       }
     },
+    "nopt": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz",
+      "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=",
+      "optional": true,
+      "requires": {
+        "abbrev": "1",
+        "osenv": "^0.1.4"
+      }
+    },
     "normalize-package-data": {
       "version": "2.5.0",
       "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
@@ -8603,6 +8899,22 @@
       "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz",
       "integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg=="
     },
+    "npm-bundled": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.6.tgz",
+      "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==",
+      "optional": true
+    },
+    "npm-packlist": {
+      "version": "1.4.4",
+      "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.4.tgz",
+      "integrity": "sha512-zTLo8UcVYtDU3gdeaFu2Xu0n0EvelfHDGuqtNIn5RO7yQj4H1TqNdBc/yZjxnWA0PVB8D3Woyp0i5B43JwQ6Vw==",
+      "optional": true,
+      "requires": {
+        "ignore-walk": "^3.0.1",
+        "npm-bundled": "^1.0.1"
+      }
+    },
     "npm-run-path": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
@@ -8611,6 +8923,18 @@
         "path-key": "^2.0.0"
       }
     },
+    "npmlog": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
+      "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
+      "optional": true,
+      "requires": {
+        "are-we-there-yet": "~1.1.2",
+        "console-control-strings": "~1.1.0",
+        "gauge": "~2.7.3",
+        "set-blocking": "~2.0.0"
+      }
+    },
     "nth-check": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz",
@@ -8835,6 +9159,12 @@
       "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz",
       "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc="
     },
+    "os-homedir": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
+      "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
+      "optional": true
+    },
     "os-locale": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz",
@@ -8850,6 +9180,16 @@
       "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
       "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
     },
+    "osenv": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
+      "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
+      "optional": true,
+      "requires": {
+        "os-homedir": "^1.0.0",
+        "os-tmpdir": "^1.0.0"
+      }
+    },
     "p-defer": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz",
@@ -10257,6 +10597,18 @@
         }
       }
     },
+    "rc": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+      "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+      "optional": true,
+      "requires": {
+        "deep-extend": "^0.6.0",
+        "ini": "~1.3.0",
+        "minimist": "^1.2.0",
+        "strip-json-comments": "~2.0.1"
+      }
+    },
     "react": {
       "version": "16.8.6",
       "resolved": "https://registry.npmjs.org/react/-/react-16.8.6.tgz",
@@ -11299,6 +11651,23 @@
       "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
       "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
     },
+    "simple-concat": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz",
+      "integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=",
+      "optional": true
+    },
+    "simple-get": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.0.3.tgz",
+      "integrity": "sha512-Wvre/Jq5vgoz31Z9stYWPLn0PqRqmBDpFSdypAnHu5AvRVCYPRYGnvryNLiXu8GOBNDH82J2FRHUGMjjHUpXFw==",
+      "optional": true,
+      "requires": {
+        "decompress-response": "^3.3.0",
+        "once": "^1.3.1",
+        "simple-concat": "^1.0.0"
+      }
+    },
     "simple-swizzle": {
       "version": "0.2.2",
       "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
@@ -11973,6 +12342,21 @@
       "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz",
       "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA=="
     },
+    "tar": {
+      "version": "4.4.10",
+      "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.10.tgz",
+      "integrity": "sha512-g2SVs5QIxvo6OLp0GudTqEf05maawKUxXru104iaayWA09551tFCTI8f1Asb4lPfkBr91k07iL4c11XO3/b0tA==",
+      "optional": true,
+      "requires": {
+        "chownr": "^1.1.1",
+        "fs-minipass": "^1.2.5",
+        "minipass": "^2.3.5",
+        "minizlib": "^1.2.1",
+        "mkdirp": "^0.5.0",
+        "safe-buffer": "^5.1.2",
+        "yallist": "^3.0.3"
+      }
+    },
     "terser": {
       "version": "3.17.0",
       "resolved": "https://registry.npmjs.org/terser/-/terser-3.17.0.tgz",
@@ -12895,6 +13279,15 @@
       "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
       "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho="
     },
+    "wide-align": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
+      "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
+      "optional": true,
+      "requires": {
+        "string-width": "^1.0.2 || 2"
+      }
+    },
     "wordwrap": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
diff --git a/package.json b/package.json
index 91b670f85cb70e95340396820573962e7fb8e5fe..1dc26e15271ed4fb049478589a5b9e8bbf7a4a01 100644
--- a/package.json
+++ b/package.json
@@ -14,7 +14,6 @@
     "react-leaflet-draw": "0.19.0",
     "react-router-dom": "^5.0.1",
     "react-scripts": "3.0.1",
-    "universal-cookie": "^4.0.0",
     "socket.io": "^2.2.0",
     "socket.io-client": "^2.2.0"
   },
diff --git a/public/infantry.svg b/public/infantry.svg
new file mode 100644
index 0000000000000000000000000000000000000000..ac9e8bcfed3b9100dcf635fd161aa2cb5a0e612a
--- /dev/null
+++ b/public/infantry.svg
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   version="1.1"
+   width="591.0625"
+   height="372.0625"
+   id="svg2">
+  <defs
+     id="defs4">
+    <linearGradient
+       id="linearGradient3777">
+      <stop
+         id="stop3779"
+         style="stop-color:#b35800;stop-opacity:1"
+         offset="0" />
+    </linearGradient>
+  </defs>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     transform="translate(-72.28125,-210.75)"
+     id="layer1">
+    <path
+       d="M 585.90976,366.37406 4.2663332,3.7357361"
+       transform="translate(72.28125,210.75)"
+       id="path3881"
+       style="fill:none;stroke:#000000;stroke-width:24;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+    <path
+       d="M 584.7946,6.0174311 5.6411055,365.7946"
+       transform="translate(72.28125,210.75)"
+       id="path3883"
+       style="fill:none;stroke:#000000;stroke-width:24;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+    <rect
+       width="583.06555"
+       height="364.06042"
+       x="76.266335"
+       y="214.73877"
+       id="rect3859"
+       style="fill:none;stroke:#ff0000;stroke-width:24;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+  </g>
+</svg>
diff --git a/public/light-infantry.svg b/public/light-infantry.svg
new file mode 100644
index 0000000000000000000000000000000000000000..4e7d1c7347c4c0b6aa04d0250fdec374a9252ba7
--- /dev/null
+++ b/public/light-infantry.svg
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" version="1.1" width="591.0625" height="372.0625" id="svg2">
+  <defs id="defs4">
+    <linearGradient id="linearGradient3777">
+      <stop id="stop3779" style="stop-color:#b35800;stop-opacity:1" offset="0"/>
+    </linearGradient>
+  </defs>
+  <metadata id="metadata7">
+    <rdf:RDF>
+      <cc:Work rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
+        <dc:title/>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g transform="translate(-72.28125,-210.75)" id="layer1">
+    <path d="M 585.90976,366.37406 4.2663332,3.7357361" transform="translate(72.28125,210.75)" id="path3881" style="fill:none;stroke:#000000;stroke-width:8;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"/>
+    <path d="M 584.7946,6.0174311 5.6411055,365.7946" transform="translate(72.28125,210.75)" id="path3883" style="fill:none;stroke:#000000;stroke-width:8;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"/>
+    <rect width="583.06555" height="364.06042" x="76.266335" y="214.73877" id="rect3859" style="fill:none;stroke:#0b04fb;stroke-width:8;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"/>
+  </g>
+  <text x="293.67578" y="344.20807" id="text2988" xml:space="preserve" style="font-size:50px;font-style:normal;font-weight:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"><tspan x="293.67578" y="344.20807" id="tspan2990" style="font-size:100px">L</tspan></text>
+</svg>
\ No newline at end of file
diff --git a/public/mechanized.svg b/public/mechanized.svg
new file mode 100644
index 0000000000000000000000000000000000000000..b3857efd4debbee06f8e812ba6e3db13c359b3fa
--- /dev/null
+++ b/public/mechanized.svg
@@ -0,0 +1,99 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="591.0625"
+   height="372.0625"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.48.4 r9939"
+   sodipodi:docname="NATO Map Symbol - Infantry.svg">
+  <defs
+     id="defs4">
+    <linearGradient
+       id="linearGradient3777"
+       osb:paint="solid">
+      <stop
+         style="stop-color:#b35800;stop-opacity:1;"
+         offset="0"
+         id="stop3779" />
+    </linearGradient>
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="1.595432"
+     inkscape:cx="295.53125"
+     inkscape:cy="186.03125"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0"
+     inkscape:window-width="1401"
+     inkscape:window-height="960"
+     inkscape:window-x="22"
+     inkscape:window-y="22"
+     inkscape:window-maximized="0" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-72.28125,-210.75)">
+    <path
+       sodipodi:type="arc"
+       style="fill:none;stroke:#000000;stroke-width:12;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+       id="path3879"
+       sodipodi:cx="294.37698"
+       sodipodi:cy="184.34384"
+       sodipodi:rx="160.69855"
+       sodipodi:ry="76.793999"
+       d="m 455.07553,184.34384 a 160.69855,76.793999 0 1 1 -321.39709,0 160.69855,76.793999 0 1 1 321.39709,0 z"
+       transform="matrix(1.4300046,0,0,1.5757504,-53.16133,106.2891)" />
+    <path
+       style="fill:none;stroke:#000000;stroke-width:24;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
+       d="M 585.90976,366.37406 4.2663332,3.7357361"
+       id="path3881"
+       inkscape:connector-curvature="0"
+       transform="translate(72.28125,210.75)" />
+    <path
+       style="fill:none;stroke:#000000;stroke-width:24;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
+       d="M 584.7946,6.0174311 5.6411055,365.7946"
+       id="path3883"
+       inkscape:connector-curvature="0"
+       transform="translate(72.28125,210.75)" />
+    <rect
+       style="fill:none;stroke:#ff0000;stroke-width:24;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+       id="rect3859"
+       width="583.06555"
+       height="364.06042"
+       x="76.266335"
+       y="214.73877" />
+  </g>
+</svg>
diff --git a/public/recon.svg b/public/recon.svg
new file mode 100644
index 0000000000000000000000000000000000000000..4e89e6116e69adc6ec87efe92c20630cb8d907f9
--- /dev/null
+++ b/public/recon.svg
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   version="1.1"
+   width="591.0625"
+   height="372.0625"
+   id="svg2"
+   inkscape:version="0.48.4 r9939"
+   sodipodi:docname="NATO Map Symbol - Infantry.svg">
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="640"
+     inkscape:window-height="480"
+     id="namedview11"
+     showgrid="false"
+     inkscape:zoom="0.53801417"
+     inkscape:cx="295.53125"
+     inkscape:cy="186.03125"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="svg2" />
+  <defs
+     id="defs4">
+    <linearGradient
+       id="linearGradient3777">
+      <stop
+         id="stop3779"
+         style="stop-color:#b35800;stop-opacity:1"
+         offset="0" />
+    </linearGradient>
+  </defs>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <path
+     inkscape:connector-curvature="0"
+     style="fill:none;stroke:#000000;stroke-width:24;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+     id="path3883"
+     d="M 584.7946,6.0174311 5.6411055,365.7946" />
+  <rect
+     style="fill:none;stroke:#ff0000;stroke-width:24;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+     id="rect3859"
+     y="3.9887695"
+     x="3.9850845"
+     height="364.06042"
+     width="583.06555" />
+</svg>
diff --git a/src/App.css b/src/App.css
index c22f442f3c1f1f3608c1605b7784a026aa79f0b4..67ec836745b270ed348f7e18493bee4f92d1922b 100644
--- a/src/App.css
+++ b/src/App.css
@@ -237,3 +237,50 @@ div.login button:hover {
   display: flex;
   flex-direction: column;
 }
+
+.leaflet-control-playback {
+  position: relative;
+  background-color: #7cbdf5;
+  padding: 10px;
+}
+.leaflet-control-playback .optionsContainer {
+  position: relative;
+}
+.leaflet-control-playback .optionsContainer > div {
+  display: inline-block;
+}
+.leaflet-control-playback .buttonContainer {
+}
+.leaflet-control-playback .buttonContainer a {
+  display: inline-block;
+  width: 32px;
+  height: 32px;
+  text-decoration: none;
+}
+.leaflet-control-playback .buttonContainer .btn-stop {
+  background: url(icons/icon-play.png) no-repeat center;
+}
+.leaflet-control-playback .buttonContainer .btn-start {
+  background: url(icons/icon-stop.png) no-repeat center;
+}
+.leaflet-control-playback .buttonContainer .btn-restart {
+  background: url(icons/icon-restart.png) no-repeat center;
+}
+.leaflet-control-playback .buttonContainer .btn-slow {
+  background: url(icons/icon-slow.png) no-repeat center;
+}
+.leaflet-control-playback .buttonContainer .btn-quick {
+  background: url(icons/icon-quick.png) no-repeat center;
+}
+.leaflet-control-playback .buttonContainer .btn-close {
+  background: url(icons/icon-close.png) no-repeat center;
+}
+.leaflet-control-playback .infoContainer {
+}
+.leaflet-control-playback .sliderContainer {
+}
+
+.leaflet-tooltip {
+  background-color: rgba(0, 0, 0, 0.5);
+  color: white;
+}
diff --git a/src/App.js b/src/App.js
index 3b57bad75dba81a4592757b60669e01685834f28..b1752f70d4e09e5afc57d112005bc247f8d70d8e 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,6 +1,8 @@
 import React, { Component } from "react";
 import "../node_modules/leaflet-draw/dist/leaflet.draw.css";
 import "./App.css";
+
+import ClientSocket from "./components/Socket";
 import {
   BrowserRouter as Router,
   Route,
@@ -11,6 +13,7 @@ import LoginForm from "./components/LoginForm";
 import RegisterForm from "./components/RegisterForm";
 import GameSelection from "./components/GameSelection";
 import GameView from "./components/GameView";
+import ReplayMap from "./components/ReplayMap";
 
 export default class App extends Component {
   constructor() {
@@ -124,6 +127,11 @@ export default class App extends Component {
       />
     );
   };
+
+  replay = () => {
+    return <ReplayMap />;
+  };
+
   render() {
     // TODO: think better solution to wait for authenticator
     if (!this.state.authenticateComplete) {
@@ -159,9 +167,9 @@ export default class App extends Component {
                 <Redirect from="*" to="/" />
               </Switch>
             )}
-
             {this.state.logged && (
               <Switch>
+                <Route exact path="/replay" component={this.replay} />
                 <Route
                   path="/game"
                   component={() => {
diff --git a/src/components/DrawGeoJSON.js b/src/components/DrawGeoJSON.js
new file mode 100644
index 0000000000000000000000000000000000000000..fd7f4f3b464f6c3c4814217db17fe1056a3fcb25
--- /dev/null
+++ b/src/components/DrawGeoJSON.js
@@ -0,0 +1,134 @@
+import React from "react";
+import L from "leaflet";
+import "leaflet-draw";
+import {
+  Circle,
+  Marker,
+  Polygon,
+  Polyline,
+  Rectangle,
+  Tooltip
+} from "react-leaflet";
+
+// an empty icon for textboxes
+let noIcon = L.divIcon({
+  className: "",
+  iconSize: [20, 20],
+  iconAnchor: [10, 20]
+});
+
+class DrawGeoJSON extends React.Component {
+  constructor(props) {
+    super(props);
+  }
+
+  render() {
+    return (
+      <React.Fragment>
+        {/* iterate through every element fetched from back-end */}
+        {this.props.geoJSONLayer.features.map(feature => {
+          let id = feature.mapDrawingHistoryId;
+          let coords = feature.data.geometry.coordinates;
+          let type = feature.data.geometry.type;
+          let color = feature.data.properties.color;
+          let radius = feature.data.properties.radius;
+          let text = feature.data.properties.text;
+          let rectangle = feature.data.properties.rectangle;
+          if (type === "Point") {
+            // GeoJSON saves latitude first, not longitude like usual. swapping
+            let position = [coords[1], coords[0]];
+            if (radius) {
+              return (
+                // keys are required to be able to edit
+                <Circle
+                  key={Math.random()}
+                  center={position}
+                  id={id}
+                  radius={radius}
+                  color={color}
+                />
+              );
+            } else if (text) {
+              return (
+                <Marker
+                  key={Math.random()}
+                  position={position}
+                  id={id}
+                  color={color}
+                  icon={noIcon}
+                >
+                  <Tooltip
+                    direction="bottom"
+                    permanent
+                    className="editable"
+                    interactive={true}
+                  >
+                    <div class="editable">
+                      <div
+                        contenteditable="true"
+                        placeholder="Click out to save"
+                      >
+                        {text}
+                      </div>
+                    </div>
+                  </Tooltip>
+                </Marker>
+              );
+            } else {
+              // unknown if color changes anything. need to test
+              return (
+                <Marker
+                  key={Math.random()}
+                  position={position}
+                  id={id}
+                  color={color}
+                />
+              );
+            }
+          } else if (rectangle) {
+            // instead of an array of four coordinates, rectangles only have two corners
+            let bounds = coords[0].map(coord => {
+              return [coord[1], coord[0]];
+            });
+            return (
+              <Rectangle
+                key={Math.random()}
+                bounds={bounds}
+                id={id}
+                color={color}
+              />
+            );
+          } else if (type === "Polygon") {
+            // Polygon coordinates are wrapped under a one element array, for some reason
+            let positions = coords[0].map(coord => {
+              return [coord[1], coord[0]];
+            });
+            return (
+              <Polygon
+                key={Math.random()}
+                positions={positions}
+                id={id}
+                color={color}
+              />
+            );
+          } else if (type === "LineString") {
+            // Polyline coordinates are a normal array, unlike Polygon
+            let positions = coords.map(coord => {
+              return [coord[1], coord[0]];
+            });
+            return (
+              <Polyline
+                key={Math.random()}
+                positions={positions}
+                id={id}
+                color={color}
+              />
+            );
+          }
+        })}
+      </React.Fragment>
+    );
+  }
+}
+
+export default DrawGeoJSON;
diff --git a/src/components/ReplayMap.js b/src/components/ReplayMap.js
new file mode 100644
index 0000000000000000000000000000000000000000..f173ffd93b7bccb67fd61e031800ccb6ac857dbe
--- /dev/null
+++ b/src/components/ReplayMap.js
@@ -0,0 +1,177 @@
+// https://github.com/linghuam/Leaflet.TrackPlayBack
+
+import React from "react";
+import L from "leaflet";
+import { Map, TileLayer, ZoomControl, Marker, Popup } from "react-leaflet";
+import "../track-playback/src/leaflet.trackplayback/clock";
+import "../track-playback/src/leaflet.trackplayback/index";
+import "../track-playback/src/control.trackplayback/control.playback";
+import "../track-playback/src/control.trackplayback/index";
+import DrawGeoJSON from "./DrawGeoJSON";
+
+export default class ReplayMap extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      // stores the playback object
+      playback: null,
+      // stores player locations from backend
+      data: null,
+      // stores all drawings from backend
+      allGeoJSON: [],
+      // stores all active drawings on the map
+      activeGeoJSON: []
+    };
+    this.map = React.createRef();
+  }
+
+  async componentDidMount() {
+    await this.fetchPlayerData();
+    //await this.fetchDrawingData();
+    //this.tickDrawings();
+    this.replay();
+  }
+
+  componentWillReceiveProps(nextProps) {}
+
+  // cloud game a1231e2b-aa29-494d-b687-ea2d48cc23df
+  // local game wimma 314338f9-b0bb-4bf7-9554-769c7b409bce
+  // local game vbox 16977b13-c419-48b4-b7d6-e7620f27bf39
+  // fetch player locations from the game
+  fetchPlayerData = async () => {
+    await fetch(
+      `${
+        process.env.REACT_APP_API_URL
+      }/replay/players/314338f9-b0bb-4bf7-9554-769c7b409bce`,
+      {
+        method: "GET"
+      }
+    )
+      .then(async res => await res.json())
+      .then(
+        async res => {
+          await this.setState({ data: res });
+        },
+        // Note: it's important to handle errors here
+        // instead of a catch() block so that we don't swallow
+        // exceptions from actual bugs in components.
+        error => {
+          console.log(error);
+        }
+      );
+  };
+
+  fetchDrawingData = async () => {
+    await fetch(
+      `${process.env.REACT_APP_API_URL}/replay/{
+		"lng": 25.72588,
+		"lat": 62.23147
+}`,
+      {
+        method: "GET"
+      }
+    )
+      .then(async res => await res.json())
+      .then(
+        async res => {
+          await this.setState({ allGeoJSON: res });
+        },
+        error => {
+          console.log(error);
+        }
+      );
+  };
+
+  tickDrawings = () => {
+    let activeDrawings = [];
+    this.state.allGeoJSON.map(drawing => {
+      activeDrawings.push(drawing[0]);
+      this.setState({
+        activeGeoJSON: {
+          type: "FeatureCollection",
+          features: [...activeDrawings]
+        }
+      });
+    });
+  };
+
+  replay = () => {
+    this.map = L.map(this.refs.map).setView([62.3, 25.7], 15);
+    L.tileLayer("https://tiles.kartat.kapsi.fi/taustakartta/{z}/{x}/{y}.jpg", {
+      attribution:
+        '&copy; <a href="https://www.maanmittauslaitos.fi/">Maanmittauslaitos</a>'
+    }).addTo(this.map);
+    this.trackplayback = new L.TrackPlayBack(this.state.data, this.map, {
+      trackPointOptions: {
+        // whether to draw track point
+        isDraw: true,
+        // whether to use canvas to draw it, if false, use leaflet api `L.circleMarker`
+        useCanvas: false,
+        stroke: true,
+        color: "#000000",
+        fill: true,
+        fillColor: "rgba(0,0,0,0)",
+        opacity: 0,
+        radius: 12
+      },
+      targetOptions: {
+        // whether to use an image to display target, if false, the program provides a default
+        useImg: true,
+        // if useImg is true, provide the imgUrl
+        imgUrl: "../light-infantry.svg",
+        // the width of target, unit: px
+        width: 60,
+        // the height of target, unit: px
+        height: 40,
+        // the stroke color of target, effective when useImg set false
+        color: "#00f",
+        // the fill color of target, effective when useImg set false
+        fillColor: "#9FD12D"
+      },
+      clockOptions: {
+        // the default speed
+        // caculate method: fpstime * Math.pow(2, speed - 1)
+        // fpstime is the two frame time difference
+        speed: 10,
+        // the max speed
+        maxSpeed: 100
+      },
+      toolTipOptions: {
+        offset: [0, 0],
+        direction: "top",
+        permanent: false
+      }
+    });
+    this.setState({
+      playback: this.trackplayback
+    });
+    this.trackplaybackControl = L.trackplaybackcontrol(this.trackplayback);
+    this.trackplaybackControl.addTo(this.map);
+  };
+
+  render() {
+    return (
+      /*       <Map
+        className="map"
+        ref={this.map}
+        center={[62.3, 25.7]}
+        zoom={15}
+        minZoom="7"
+        maxZoom="17"
+        zoomControl={false}
+      >
+        <TileLayer
+          attribution='&copy; <a href="https://www.maanmittauslaitos.fi/">Maanmittauslaitos</a>'
+          url={"https://tiles.kartat.kapsi.fi/taustakartta/{z}/{x}/{y}.jpg"}
+        />
+        <ZoomControl position="topright" />
+        {this.state.activeGeoJSON.features && (
+          <DrawGeoJSON geoJSONLayer={this.state.activeGeoJSON} />
+        )}
+      </Map> */
+      <React.Fragment>
+        <div className="map" ref="map" />
+      </React.Fragment>
+    );
+  }
+}
diff --git a/src/icons/icon-close.png b/src/icons/icon-close.png
new file mode 100644
index 0000000000000000000000000000000000000000..81bef1ad48176c82033dcb527940cff736a53818
Binary files /dev/null and b/src/icons/icon-close.png differ
diff --git a/src/icons/icon-play.png b/src/icons/icon-play.png
new file mode 100644
index 0000000000000000000000000000000000000000..3af78948ec238cc2fc13e5a2e16569c516b1cd22
Binary files /dev/null and b/src/icons/icon-play.png differ
diff --git a/src/icons/icon-quick.png b/src/icons/icon-quick.png
new file mode 100644
index 0000000000000000000000000000000000000000..3dc45fd114d7f5813be62cebde2bd95ccedcdceb
Binary files /dev/null and b/src/icons/icon-quick.png differ
diff --git a/src/icons/icon-restart.png b/src/icons/icon-restart.png
new file mode 100644
index 0000000000000000000000000000000000000000..022c4e60fdec44ee1a2d605c85b1f156c16c7cb6
Binary files /dev/null and b/src/icons/icon-restart.png differ
diff --git a/src/icons/icon-slow.png b/src/icons/icon-slow.png
new file mode 100644
index 0000000000000000000000000000000000000000..eaa1fbdaf382287502240e0987e1df0b8840ee8e
Binary files /dev/null and b/src/icons/icon-slow.png differ
diff --git a/src/icons/icon-stop.png b/src/icons/icon-stop.png
new file mode 100644
index 0000000000000000000000000000000000000000..3ee3137966264afc7119579bf52a7a1bb457e753
Binary files /dev/null and b/src/icons/icon-stop.png differ
diff --git a/src/track-playback/src/control.trackplayback/control.playback.js b/src/track-playback/src/control.trackplayback/control.playback.js
new file mode 100644
index 0000000000000000000000000000000000000000..6c6f101980579abf0abab21b9d32c2d4626abca4
--- /dev/null
+++ b/src/track-playback/src/control.trackplayback/control.playback.js
@@ -0,0 +1,319 @@
+import L from "leaflet";
+
+export const TrackPlayBackControl = L.Control.extend({
+  options: {
+    position: "topright",
+    showOptions: true,
+    showInfo: true,
+    showSlider: true,
+    autoPlay: false
+  },
+
+  initialize: function(trackplayback, options) {
+    L.Control.prototype.initialize.call(this, options);
+    this.trackPlayBack = trackplayback;
+    this.trackPlayBack.on("tick", this._tickCallback, this);
+  },
+
+  onAdd: function(map) {
+    this._initContainer();
+    return this._container;
+  },
+
+  onRemove: function(map) {
+    this.trackPlayBack.dispose();
+    this.trackPlayBack.off("tick", this._tickCallback, this);
+  },
+
+  /**
+   * 根据unix时间戳(单位:秒)获取时间字符串
+   * @param  {[int]} time     [时间戳(精确到秒)]
+   * @param  {[string]} accuracy [精度,日:d, 小时:h,分钟:m,秒:s]
+   * @return {[string]}          [yy:mm:dd hh:mm:ss]
+   */
+  getTimeStrFromUnix: function(time, accuracy = "s") {
+    return `
+      ${new Date(time).toLocaleDateString("en-US")} 
+      ${new Date(time * 1e3).toISOString().slice(-13, -5)}
+    `;
+    /*     time = parseInt(time * 1000);
+    let newDate = new Date(time);
+    let year = newDate.getFullYear();
+    let month =
+      newDate.getMonth() + 1 < 10
+        ? "0" + (newDate.getMonth() + 1)
+        : newDate.getMonth() + 1;
+    let day =
+      newDate.getDate() < 10 ? "0" + newDate.getDate() : newDate.getDate();
+    let hours =
+      newDate.getHours() < 10 ? "0" + newDate.getHours() : newDate.getHours();
+    let minuts =
+      newDate.getMinutes() < 10
+        ? "0" + newDate.getMinutes()
+        : newDate.getMinutes();
+    let seconds =
+      newDate.getSeconds() < 10
+        ? "0" + newDate.getSeconds()
+        : newDate.getSeconds();
+    let ret;
+    if (accuracy === "d") {
+      ret = year + "-" + month + "-" + day;
+    } else if (accuracy === "h") {
+      ret = year + "-" + month + "-" + day + " " + hours;
+    } else if (accuracy === "m") {
+      ret = year + "-" + month + "-" + day + " " + hours + ":" + minuts;
+    } else {
+      ret =
+        year +
+        "-" +
+        month +
+        "-" +
+        day +
+        " " +
+        hours +
+        ":" +
+        minuts +
+        ":" +
+        seconds;
+    }
+    return ret; */
+  },
+
+  _initContainer: function() {
+    var className = "leaflet-control-playback";
+    this._container = L.DomUtil.create("div", className);
+    L.DomEvent.disableClickPropagation(this._container);
+
+    this._optionsContainer = this._createContainer(
+      "optionsContainer",
+      this._container
+    );
+    this._buttonContainer = this._createContainer(
+      "buttonContainer",
+      this._container
+    );
+    this._infoContainer = this._createContainer(
+      "infoContainer",
+      this._container
+    );
+    this._sliderContainer = this._createContainer(
+      "sliderContainer",
+      this._container
+    );
+    this._lineCbx = this._createCheckbox(
+      "show trackLine",
+      "show-trackLine",
+      this._optionsContainer,
+      this._showTrackLine
+    );
+
+    this._playBtn = this._createButton(
+      "play",
+      "btn-stop",
+      this._buttonContainer,
+      this._play
+    );
+    this._restartBtn = this._createButton(
+      "replay",
+      "btn-restart",
+      this._buttonContainer,
+      this._restart
+    );
+    this._slowSpeedBtn = this._createButton(
+      "slow",
+      "btn-slow",
+      this._buttonContainer,
+      this._slow
+    );
+    this._quickSpeedBtn = this._createButton(
+      "quick",
+      "btn-quick",
+      this._buttonContainer,
+      this._quick
+    );
+    /*     this._closeBtn = this._createButton(
+      "close",
+      "btn-close",
+      this._buttonContainer,
+      this._close
+    ); */
+
+    this._infoStartTime = this._createInfo(
+      "Game started: ",
+      this.getTimeStrFromUnix(this.trackPlayBack.getStartTime()),
+      "info-start-time",
+      this._infoContainer
+    );
+    this._infoEndTime = this._createInfo(
+      "Game ended: ",
+      this.getTimeStrFromUnix(this.trackPlayBack.getEndTime()),
+      "info-end-time",
+      this._infoContainer
+    );
+    this._infoCurTime = this._createInfo(
+      "Current time: ",
+      this.getTimeStrFromUnix(this.trackPlayBack.getCurTime()),
+      "info-cur-time",
+      this._infoContainer
+    );
+    this._infoSpeedRatio = this._createInfo(
+      "speed: ",
+      `X${this.trackPlayBack.getSpeed()}`,
+      "info-speed-ratio",
+      this._infoContainer
+    );
+
+    this._slider = this._createSlider(
+      "time-slider",
+      this._sliderContainer,
+      this._scrollchange
+    );
+
+    return this._container;
+  },
+
+  _createContainer: function(className, container) {
+    return L.DomUtil.create("div", className, container);
+  },
+
+  _createCheckbox: function(labelName, className, container, fn) {
+    let divEle = L.DomUtil.create(
+      "div",
+      className + " trackplayback-checkbox",
+      container
+    );
+
+    let inputEle = L.DomUtil.create("input", "trackplayback-input", divEle);
+    let inputId = `trackplayback-input-${L.Util.stamp(inputEle)}`;
+    inputEle.setAttribute("type", "checkbox");
+    inputEle.setAttribute("id", inputId);
+
+    let labelEle = L.DomUtil.create("label", "trackplayback-label", divEle);
+    labelEle.setAttribute("for", inputId);
+    labelEle.innerHTML = labelName;
+
+    L.DomEvent.on(inputEle, "change", fn, this);
+
+    return divEle;
+  },
+
+  _createButton: function(title, className, container, fn) {
+    let link = L.DomUtil.create("a", className, container);
+    link.href = "#";
+    link.title = title;
+
+    /*
+     * Will force screen readers like VoiceOver to read this as "Zoom in - button"
+     */
+    link.setAttribute("role", "button");
+    link.setAttribute("aria-label", title);
+
+    L.DomEvent.disableClickPropagation(link);
+    L.DomEvent.on(link, "click", fn, this);
+
+    return link;
+  },
+
+  _createInfo: function(title, info, className, container) {
+    let infoContainer = L.DomUtil.create("div", "info-container", container);
+    let infoTitle = L.DomUtil.create("span", "info-title", infoContainer);
+    infoTitle.innerHTML = title;
+    let infoEle = L.DomUtil.create("span", className, infoContainer);
+    infoEle.innerHTML = info;
+    return infoEle;
+  },
+
+  _createSlider: function(className, container, fn) {
+    let sliderEle = L.DomUtil.create("input", className, container);
+    sliderEle.setAttribute("type", "range");
+    sliderEle.setAttribute("min", this.trackPlayBack.getStartTime());
+    sliderEle.setAttribute("max", this.trackPlayBack.getEndTime());
+    sliderEle.setAttribute("value", this.trackPlayBack.getCurTime());
+
+    L.DomEvent.on(
+      sliderEle,
+      "click mousedown dbclick",
+      L.DomEvent.stopPropagation
+    )
+      .on(sliderEle, "click", L.DomEvent.preventDefault)
+      .on(sliderEle, "change", fn, this)
+      .on(sliderEle, "mousemove", fn, this);
+
+    return sliderEle;
+  },
+
+  _showTrackLine(e) {
+    if (e.target.checked) {
+      this.trackPlayBack.showTrackLine();
+    } else {
+      this.trackPlayBack.hideTrackLine();
+    }
+  },
+
+  _play: function() {
+    let hasClass = L.DomUtil.hasClass(this._playBtn, "btn-stop");
+    if (hasClass) {
+      L.DomUtil.removeClass(this._playBtn, "btn-stop");
+      L.DomUtil.addClass(this._playBtn, "btn-start");
+      this._playBtn.setAttribute("title", "stop");
+      this.trackPlayBack.start();
+    } else {
+      L.DomUtil.removeClass(this._playBtn, "btn-start");
+      L.DomUtil.addClass(this._playBtn, "btn-stop");
+      this._playBtn.setAttribute("title", "play");
+      this.trackPlayBack.stop();
+    }
+  },
+
+  _restart: function() {
+    // 播放开始改变播放按钮样式
+    L.DomUtil.removeClass(this._playBtn, "btn-stop");
+    L.DomUtil.addClass(this._playBtn, "btn-start");
+    this._playBtn.setAttribute("title", "stop");
+    this.trackPlayBack.rePlaying();
+  },
+
+  _slow: function() {
+    this.trackPlayBack.slowSpeed();
+    let sp = this.trackPlayBack.getSpeed();
+    this._infoSpeedRatio.innerHTML = `X${sp}`;
+  },
+
+  _quick: function() {
+    this.trackPlayBack.quickSpeed();
+    let sp = this.trackPlayBack.getSpeed();
+    this._infoSpeedRatio.innerHTML = `X${sp}`;
+  },
+
+  _close: function() {
+    L.DomUtil.remove(this._container);
+    if (this.onRemove) {
+      this.onRemove(this._map);
+    }
+    return this;
+  },
+
+  _scrollchange: function(e) {
+    let val = Number(e.target.value);
+    this.trackPlayBack.setCursor(val);
+  },
+
+  _tickCallback: function(e) {
+    // 更新时间
+    let time = this.getTimeStrFromUnix(e.time);
+    this._infoCurTime.innerHTML = time;
+    // 更新时间轴
+    this._slider.value = e.time;
+    // 播放结束后改变播放按钮样式
+    if (e.time >= this.trackPlayBack.getEndTime()) {
+      L.DomUtil.removeClass(this._playBtn, "btn-start");
+      L.DomUtil.addClass(this._playBtn, "btn-stop");
+      this._playBtn.setAttribute("title", "play");
+      this.trackPlayBack.stop();
+    }
+  }
+});
+
+export const trackplaybackcontrol = function(trackplayback, options) {
+  return new TrackPlayBackControl(trackplayback, options);
+};
diff --git a/src/track-playback/src/control.trackplayback/index.js b/src/track-playback/src/control.trackplayback/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..599a2df5932224ccd4af7d3146dfa8d078d9c6ab
--- /dev/null
+++ b/src/track-playback/src/control.trackplayback/index.js
@@ -0,0 +1,5 @@
+import L from "leaflet";
+import { TrackPlayBackControl, trackplaybackcontrol } from "./control.playback";
+
+L.TrackPlayBackControl = TrackPlayBackControl;
+L.trackplaybackcontrol = trackplaybackcontrol;
diff --git a/src/track-playback/src/leaflet.trackplayback/clock.js b/src/track-playback/src/leaflet.trackplayback/clock.js
new file mode 100644
index 0000000000000000000000000000000000000000..83ff85bdcfeb6f7e85707282e250052deb37c622
--- /dev/null
+++ b/src/track-playback/src/leaflet.trackplayback/clock.js
@@ -0,0 +1,132 @@
+import L from "leaflet";
+/**
+ * 时钟类,控制轨迹播放动画
+ */
+export const Clock = L.Class.extend({
+  includes: L.Mixin.Events,
+
+  options: {
+    // 播放速度
+    // 计算方法 fpstime * Math.pow(2, this._speed - 1)
+    speed: 12,
+    // 最大播放速度
+    maxSpeed: 65
+  },
+
+  initialize: function(trackController, options) {
+    L.setOptions(this, options);
+
+    this._trackController = trackController;
+    this._endTime = this._trackController.getMaxTime();
+    this._curTime = this._trackController.getMinTime();
+    this._speed = this.options.speed;
+    this._maxSpeed = this.options.maxSpeed;
+    this._intervalID = null;
+    this._lastFpsUpdateTime = 0;
+  },
+
+  start: function() {
+    if (this._intervalID) return;
+    this._intervalID = L.Util.requestAnimFrame(this._tick, this);
+  },
+
+  stop: function() {
+    if (!this._intervalID) return;
+    L.Util.cancelAnimFrame(this._intervalID);
+    this._intervalID = null;
+    this._lastFpsUpdateTime = 0;
+  },
+
+  rePlaying: function() {
+    this.stop();
+    this._curTime = this._trackController.getMinTime();
+    this.start();
+  },
+
+  slowSpeed: function() {
+    this._speed = this._speed <= 1 ? this._speed : this._speed - 1;
+    if (this._intervalID) {
+      this.stop();
+      this.start();
+    }
+  },
+
+  quickSpeed: function() {
+    this._speed = this._speed >= this._maxSpeed ? this._speed : this._speed + 1;
+    if (this._intervalID) {
+      this.stop();
+      this.start();
+    }
+  },
+
+  getSpeed: function() {
+    return this._speed;
+  },
+
+  getCurTime: function() {
+    return this._curTime;
+  },
+
+  getStartTime: function() {
+    return this._trackController.getMinTime();
+  },
+
+  getEndTime: function() {
+    return this._trackController.getMaxTime();
+  },
+
+  isPlaying: function() {
+    return !!this._intervalID;
+  },
+
+  setCursor: function(time) {
+    this._curTime = time;
+    this._trackController.drawTracksByTime(this._curTime);
+    this.fire("tick", {
+      time: this._curTime
+    });
+  },
+
+  setSpeed: function(speed) {
+    this._speed = speed;
+    if (this._intervalID) {
+      this.stop();
+      this.start();
+    }
+  },
+
+  // 计算两帧时间间隔,单位:秒
+  _caculatefpsTime: function(now) {
+    let time;
+    if (this._lastFpsUpdateTime === 0) {
+      time = 0;
+    } else {
+      time = now - this._lastFpsUpdateTime;
+    }
+    this._lastFpsUpdateTime = now;
+    // 将毫秒转换成秒
+    time = time / 1000;
+    return time;
+  },
+
+  _tick: function() {
+    let now = +new Date();
+    let fpstime = this._caculatefpsTime(now);
+    let isPause = false;
+    let stepTime = fpstime * Math.pow(2, this._speed - 1);
+    this._curTime += stepTime;
+    if (this._curTime >= this._endTime) {
+      this._curTime = this._endTime;
+      isPause = true;
+    }
+    this._trackController.drawTracksByTime(this._curTime);
+    this.fire("tick", {
+      time: this._curTime
+    });
+    if (!isPause) this._intervalID = L.Util.requestAnimFrame(this._tick, this);
+  }
+});
+
+export const clock = function(trackController, options) {
+  return new Clock(trackController, options);
+};
diff --git a/src/track-playback/src/leaflet.trackplayback/draw.js b/src/track-playback/src/leaflet.trackplayback/draw.js
new file mode 100644
index 0000000000000000000000000000000000000000..ba05a447d31c1f556076c475519b2401a460d501
--- /dev/null
+++ b/src/track-playback/src/leaflet.trackplayback/draw.js
@@ -0,0 +1,380 @@
+import L from "leaflet";
+
+import { TrackLayer } from "./tracklayer";
+
+/**
+ * 绘制类
+ * 完成轨迹线、轨迹点、目标物的绘制工作
+ */
+export const Draw = L.Class.extend({
+  trackPointOptions: {
+    isDraw: false,
+    useCanvas: true,
+    stroke: false,
+    color: "#ef0300",
+    fill: true,
+    fillColor: "#ef0300",
+    opacity: 0.3,
+    radius: 4
+  },
+  trackLineOptions: {
+    isDraw: false,
+    stroke: true,
+    color: "#1C54E2", // stroke color
+    weight: 2,
+    fill: false,
+    fillColor: "#000",
+    opacity: 0.3
+  },
+  targetOptions: {
+    useImg: false,
+    imgUrl: "../../static/images/ship.png",
+    showText: false,
+    width: 8,
+    height: 18,
+    color: "#00f", // stroke color
+    fillColor: "#9FD12D"
+  },
+  toolTipOptions: {
+    offset: [0, 0],
+    direction: "top",
+    permanent: false
+  },
+
+  initialize: function(map, options) {
+    L.extend(this.trackPointOptions, options.trackPointOptions);
+    L.extend(this.trackLineOptions, options.trackLineOptions);
+    L.extend(this.targetOptions, options.targetOptions);
+    L.extend(this.toolTipOptions, options.toolTipOptions);
+
+    this._showTrackPoint = this.trackPointOptions.isDraw;
+    this._showTrackLine = this.trackLineOptions.isDraw;
+
+    this._map = map;
+    this._map.on("click", this._onmouseclickEvt, this);
+    this._map.on("mousemove", this._onmousemoveEvt, this);
+
+    this._trackLayer = new TrackLayer().addTo(map);
+    this._trackLayer.on("update", this._trackLayerUpdate, this);
+
+    this._canvas = this._trackLayer.getContainer();
+    this._ctx = this._canvas.getContext("2d");
+
+    this._bufferTracks = [];
+
+    if (!this.trackPointOptions.useCanvas) {
+      this._trackPointFeatureGroup = L.featureGroup([]).addTo(map);
+    }
+
+    // setup array for images
+    this._targetImg = [];
+  },
+
+  update: function() {
+    this._trackLayerUpdate();
+  },
+
+  drawTrack: function(trackpoints) {
+    this._bufferTracks.push(trackpoints);
+    this._drawTrack(trackpoints);
+  },
+
+  showTrackPoint: function() {
+    this._showTrackPoint = true;
+    this.update();
+  },
+
+  hideTrackPoint: function() {
+    this._showTrackPoint = false;
+    this.update();
+  },
+
+  showTrackLine: function() {
+    this._showTrackLine = true;
+    this.update();
+  },
+
+  hideTrackLine: function() {
+    this._showTrackLine = false;
+    this.update();
+  },
+
+  remove: function() {
+    this._bufferTracks = [];
+    this._trackLayer.off("update", this._trackLayerUpdate, this);
+    this._map.off("click", this._onmouseclickEvt, this);
+    this._map.off("mousemove", this._onmousemoveEvt, this);
+    if (this._map.hasLayer(this._trackLayer)) {
+      this._map.removeLayer(this._trackLayer);
+    }
+    if (this._map.hasLayer(this._trackPointFeatureGroup)) {
+      this._map.removeLayer(this._trackPointFeatureGroup);
+    }
+  },
+
+  clear: function() {
+    this._clearLayer();
+    this._bufferTracks = [];
+  },
+
+  _trackLayerUpdate: function() {
+    if (this._bufferTracks.length) {
+      this._clearLayer();
+      this._bufferTracks.forEach(
+        function(element, index) {
+          this._drawTrack(element);
+        }.bind(this)
+      );
+    }
+  },
+
+  // changes cursor icon to pointer and shows information about tracked player
+  _onmousemoveEvt: function(e) {
+    if (!this._showTrackPoint) {
+      return;
+    }
+    let point = e.layerPoint;
+    if (this._bufferTracks.length) {
+      for (let i = 0, leni = this._bufferTracks.length; i < leni; i++) {
+        for (let j = 0, len = this._bufferTracks[i].length; j < len; j++) {
+          let tpoint = this._getLayerPoint(this._bufferTracks[i][j]);
+          if (point.distanceTo(tpoint) <= this.trackPointOptions.radius) {
+            this._canvas.style.cursor = "pointer";
+            return;
+          }
+        }
+      }
+    }
+    this._canvas.style.cursor = "grab";
+  },
+
+  // on click event that shows popup about tracked player
+  _onmouseclickEvt: function(e) {
+    if (!this._showTrackPoint) {
+      return;
+    }
+    let point = e.layerPoint;
+    if (this._bufferTracks.length) {
+      for (let i = 0, leni = this._bufferTracks.length; i < leni; i++) {
+        for (let j = 0, len = this._bufferTracks[i].length; j < len; j++) {
+          let tpoint = this._getLayerPoint(this._bufferTracks[i][j]);
+          if (point.distanceTo(tpoint) <= this.trackPointOptions.radius) {
+            this._opentoolTip(this._bufferTracks[i][j]);
+            return;
+          }
+        }
+      }
+    }
+    if (this._map.hasLayer(this._tooltip)) {
+      this._map.removeLayer(this._tooltip);
+    }
+    this._canvas.style.cursor = "pointer";
+  },
+
+  _opentoolTip: function(trackpoint) {
+    if (this._map.hasLayer(this._tooltip)) {
+      this._map.removeLayer(this._tooltip);
+    }
+    let latlng = L.latLng(trackpoint.lat, trackpoint.lng);
+    let tooltip = (this._tooltip = L.tooltip(this.toolTipOptions));
+    tooltip.setLatLng(latlng);
+    tooltip.addTo(this._map);
+    tooltip.setContent(this._getTooltipText(trackpoint));
+  },
+
+  _drawTrack: function(trackpoints) {
+    // 画轨迹线
+    if (this._showTrackLine) {
+      this._drawTrackLine(trackpoints);
+    }
+    // 画船
+    let targetPoint = trackpoints[trackpoints.length - 1];
+    let info = trackpoints[0].info;
+    if (this.targetOptions.useImg && this._targetImg) {
+      this._drawShipImage(targetPoint, info);
+    } else {
+      this._drawShipCanvas(targetPoint);
+    }
+    // 画标注信息
+    if (this.targetOptions.showText) {
+      this._drawtxt(`航向:${parseInt(targetPoint.dir)}度`, targetPoint);
+    }
+    // 画经过的轨迹点
+    if (this._showTrackPoint) {
+      if (this.trackPointOptions.useCanvas) {
+        this._drawTrackPointsCanvas(trackpoints);
+      } else {
+        this._drawTrackPointsSvg(trackpoints);
+      }
+    }
+  },
+
+  _drawTrackLine: function(trackpoints) {
+    let options = this.trackLineOptions;
+    let tp0 = this._getLayerPoint(trackpoints[0]);
+    this._ctx.save();
+    this._ctx.beginPath();
+    // 画轨迹线
+    this._ctx.moveTo(tp0.x, tp0.y);
+    for (let i = 1, len = trackpoints.length; i < len; i++) {
+      let tpi = this._getLayerPoint(trackpoints[i]);
+      this._ctx.lineTo(tpi.x, tpi.y);
+    }
+    this._ctx.globalAlpha = options.opacity;
+    if (options.stroke) {
+      this._ctx.strokeStyle = options.color;
+      this._ctx.lineWidth = options.weight;
+      this._ctx.stroke();
+    }
+    if (options.fill) {
+      this._ctx.fillStyle = options.fillColor;
+      this._ctx.fill();
+    }
+    this._ctx.restore();
+  },
+
+  _drawTrackPointsCanvas: function(trackpoints) {
+    let options = this.trackPointOptions;
+    let i = trackpoints.length - 1;
+    this._ctx.save();
+    let latLng = L.latLng(trackpoints[i].lat, trackpoints[i].lng);
+    let radius = options.radius;
+    let point = this._map.latLngToLayerPoint(latLng);
+    this._ctx.beginPath();
+    this._ctx.arc(point.x, point.y, radius, 0, Math.PI * 2, false);
+    this._ctx.globalAlpha = options.opacity;
+    if (options.stroke) {
+      this._ctx.strokeStyle = options.color;
+      this._ctx.stroke();
+    }
+    if (options.fill) {
+      this._ctx.fillStyle = options.fillColor;
+      this._ctx.fill();
+    }
+    this._ctx.restore();
+  },
+
+  _drawTrackPointsSvg: function(trackpoints) {
+    let i = trackpoints.length - 1;
+    let latLng = L.latLng(trackpoints[i].lat, trackpoints[i].lng);
+    let cricleMarker = L.circleMarker(latLng, this.trackPointOptions);
+    cricleMarker.bindTooltip(
+      this._getTooltipText(trackpoints[0]),
+      this.toolTipOptions
+    );
+    this._trackPointFeatureGroup.addLayer(cricleMarker);
+  },
+
+  _drawtxt: function(text, trackpoint) {
+    let point = this._getLayerPoint(trackpoint);
+    this._ctx.save();
+    this._ctx.font = "12px Verdana";
+    this._ctx.fillStyle = "#000";
+    this._ctx.textAlign = "center";
+    this._ctx.textBaseline = "bottom";
+    this._ctx.fillText(text, point.x, point.y - 12, 200);
+    this._ctx.restore();
+  },
+
+  _drawShipCanvas: function(trackpoint) {
+    let point = this._getLayerPoint(trackpoint);
+    let rotate = trackpoint.dir || 0;
+    let w = this.targetOptions.width;
+    let h = this.targetOptions.height;
+    let dh = h / 3;
+
+    this._ctx.save();
+    this._ctx.fillStyle = this.targetOptions.fillColor;
+    this._ctx.strokeStyle = this.targetOptions.color;
+    this._ctx.translate(point.x, point.y);
+    this._ctx.rotate((Math.PI / 180) * rotate);
+    this._ctx.beginPath();
+    this._ctx.moveTo(0, 0 - h / 2);
+    this._ctx.lineTo(0 - w / 2, 0 - h / 2 + dh);
+    this._ctx.lineTo(0 - w / 2, 0 + h / 2);
+    this._ctx.lineTo(0 + w / 2, 0 + h / 2);
+    this._ctx.lineTo(0 + w / 2, 0 - h / 2 + dh);
+    this._ctx.closePath();
+    this._ctx.fill();
+    this._ctx.stroke();
+    this._ctx.restore();
+  },
+
+  // used to draw image for tracking data
+  _drawShipImage: function(trackpoint, info) {
+    let point = this._getLayerPoint(trackpoint);
+    let width = this.targetOptions.width;
+    let height = this.targetOptions.height;
+    let offset = {
+      x: width / 2,
+      y: height / 2
+    };
+    this._ctx.save();
+    this._ctx.translate(point.x, point.y);
+    let image;
+    // use an existing image if it has the same icon as the new data
+    this._targetImg.map(img => {
+      if (img.icon == info[0]["value"]) {
+        image = img;
+      }
+    });
+    // else create a new global image with new icon
+    if (!image) {
+      let img = new Image();
+      img.onload = () => {
+        this._targetImg.push(img);
+      };
+      img.onerror = () => {
+        throw new Error("img load error!");
+      };
+      img.src = info[0]["value"];
+      img.icon = info[0]["value"];
+      image = img;
+    }
+    this._ctx.drawImage(image, 0 - offset.x, 0 - offset.y, width, height);
+    // draw rect based on faction colour
+    this._ctx.strokeStyle = info[1]["value"];
+    this._ctx.lineWidth = 3;
+    this._ctx.strokeRect(0 - offset.x, 0 - offset.y, width, height);
+    this._ctx.restore();
+  },
+
+  _getTooltipText: function(targetobj) {
+    let content = [];
+    content.push("<table>");
+    if (targetobj.info && targetobj.info.length) {
+      // skip first two as they're icon and faction colour
+      for (let i = 2, len = targetobj.info.length; i < len; i++) {
+        content.push("<tr>");
+        content.push("<td>" + targetobj.info[i].value + "</td>");
+        content.push("</tr>");
+      }
+    }
+    content.push("</table>");
+    content = content.join("");
+    return content;
+  },
+
+  _clearLayer: function() {
+    let bounds = this._trackLayer.getBounds();
+    if (bounds) {
+      let size = bounds.getSize();
+      this._ctx.clearRect(bounds.min.x, bounds.min.y, size.x, size.y);
+    } else {
+      this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
+    }
+    if (this._map.hasLayer(this._trackPointFeatureGroup)) {
+      this._trackPointFeatureGroup.clearLayers();
+    }
+  },
+
+  _getLayerPoint(trackpoint) {
+    return this._map.latLngToLayerPoint(
+      L.latLng(trackpoint.lat, trackpoint.lng)
+    );
+  }
+});
+
+export const draw = function(map, options) {
+  return new Draw(map, options);
+};
diff --git a/src/track-playback/src/leaflet.trackplayback/index.js b/src/track-playback/src/leaflet.trackplayback/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..defba9838814140a2047a331ff2825ddd0e22e1e
--- /dev/null
+++ b/src/track-playback/src/leaflet.trackplayback/index.js
@@ -0,0 +1,9 @@
+import L from 'leaflet'
+
+import {
+  TrackPlayBack,
+  trackplayback
+} from './trackplayback'
+
+L.TrackPlayBack = TrackPlayBack
+L.trackplayback = trackplayback
diff --git a/src/track-playback/src/leaflet.trackplayback/track.js b/src/track-playback/src/leaflet.trackplayback/track.js
new file mode 100644
index 0000000000000000000000000000000000000000..c770c3f3e61a3b70b964be786f9cd77dcd39467f
--- /dev/null
+++ b/src/track-playback/src/leaflet.trackplayback/track.js
@@ -0,0 +1,166 @@
+import L from "leaflet";
+
+import { isArray } from "./util";
+
+/**
+ * 轨迹类
+ */
+export const Track = L.Class.extend({
+  initialize: function(trackData = [], options) {
+    L.setOptions(this, options);
+
+    trackData.forEach(item => {
+      // 添加 isOrigin 字段用来标识是否是原始采样点,与插值点区分开
+      item.isOrigin = true;
+    });
+    this._trackPoints = trackData;
+    this._timeTick = {};
+    this._update();
+  },
+
+  addTrackPoint: function(trackPoint) {
+    if (isArray(trackPoint)) {
+      for (let i = 0, len = trackPoint.length; i < len; i++) {
+        this.addTrackPoint(trackPoint[i]);
+      }
+    }
+    this._addTrackPoint(trackPoint);
+  },
+
+  getTimes: function() {
+    let times = [];
+    for (let i = 0, len = this._trackPoints.length; i < len; i++) {
+      times.push(this._trackPoints[i].time);
+    }
+    return times;
+  },
+
+  getStartTrackPoint: function() {
+    return this._trackPoints[0];
+  },
+
+  getEndTrackPoint: function() {
+    return this._trackPoints[this._trackPoints.length - 1];
+  },
+
+  getTrackPointByTime: function(time) {
+    return this._trackPoints[this._timeTick[time]];
+  },
+
+  _getCalculateTrackPointByTime: function(time) {
+    // 先判断最后一个点是否为原始点
+    let endpoint = this.getTrackPointByTime(time);
+    let startPt = this.getStartTrackPoint();
+    let endPt = this.getEndTrackPoint();
+    let times = this.getTimes();
+    if (time < startPt.time || time > endPt.time) return;
+    let left = 0;
+    let right = times.length - 1;
+    let n;
+    // 处理只有一个点情况
+    if (left === right) {
+      return endpoint;
+    }
+    // 通过【二分查找】法查出当前时间所在的时间区间
+    while (right - left !== 1) {
+      n = parseInt((left + right) / 2);
+      if (time > times[n]) left = n;
+      else right = n;
+    }
+
+    let t0 = times[left];
+    let t1 = times[right];
+    let t = time;
+    let p0 = this.getTrackPointByTime(t0);
+    let p1 = this.getTrackPointByTime(t1);
+    startPt = L.point(p0.lng, p0.lat);
+    endPt = L.point(p1.lng, p1.lat);
+    let s = startPt.distanceTo(endPt);
+    // 不同时间在同一个点情形
+    if (s <= 0) {
+      endpoint = p1;
+      return endpoint;
+    }
+    // 假设目标在两点间做匀速直线运动
+    // 求解速度向量,并计算时间 t 目标所在位置
+    let v = s / (t1 - t0);
+    let sinx = (endPt.y - startPt.y) / s;
+    let cosx = (endPt.x - startPt.x) / s;
+    let step = v * (t - t0);
+    let x = startPt.x + step * cosx;
+    let y = startPt.y + step * sinx;
+    // 求目标的运动方向,0-360度
+    let dir =
+      endPt.x >= startPt.x
+        ? ((Math.PI * 0.5 - Math.asin(sinx)) * 180) / Math.PI
+        : ((Math.PI * 1.5 + Math.asin(sinx)) * 180) / Math.PI;
+
+    if (endpoint) {
+      if (endpoint.dir === undefined) {
+        endpoint.dir = dir;
+      }
+    } else {
+      endpoint = {
+        lng: x,
+        lat: y,
+        dir: endPt.dir || dir,
+        isOrigin: false,
+        time: time
+      };
+    }
+    return endpoint;
+  },
+
+  // 获取某个时间点之前走过的轨迹
+  getTrackPointsBeforeTime: function(time) {
+    let tpoints = [];
+    for (let i = 0, len = this._trackPoints.length; i < len; i++) {
+      if (this._trackPoints[i].time < time) {
+        tpoints.push(this._trackPoints[i]);
+      }
+    }
+    // 获取最后一个点,根据时间线性插值而来
+    let endPt = this._getCalculateTrackPointByTime(time);
+    if (endPt) {
+      tpoints.push(endPt);
+    }
+    return tpoints;
+  },
+
+  _addTrackPoint: function(trackPoint) {
+    trackPoint.isOrigin = true;
+    this._trackPoints.push(trackPoint);
+    this._update();
+  },
+
+  _update: function() {
+    this._sortTrackPointsByTime();
+    this._updatetimeTick();
+  },
+
+  // 轨迹点按时间排序 【冒泡排序】
+  _sortTrackPointsByTime: function() {
+    let len = this._trackPoints.length;
+    for (let i = 0; i < len; i++) {
+      for (let j = 0; j < len - 1 - i; j++) {
+        if (this._trackPoints[j].time > this._trackPoints[j + 1].time) {
+          let tmp = this._trackPoints[j + 1];
+          this._trackPoints[j + 1] = this._trackPoints[j];
+          this._trackPoints[j] = tmp;
+        }
+      }
+    }
+  },
+
+  // 为轨迹点建立时间索引,优化查找性能
+  _updatetimeTick: function() {
+    this._timeTick = {};
+    for (let i = 0, len = this._trackPoints.length; i < len; i++) {
+      this._timeTick[this._trackPoints[i].time] = i;
+    }
+  }
+});
+
+export const track = function(trackData, options) {
+  return new Track(trackData, options);
+};
diff --git a/src/track-playback/src/leaflet.trackplayback/trackcontroller.js b/src/track-playback/src/leaflet.trackplayback/trackcontroller.js
new file mode 100644
index 0000000000000000000000000000000000000000..2e8157437f541500e2aa0774329d2c9969a7d6ce
--- /dev/null
+++ b/src/track-playback/src/leaflet.trackplayback/trackcontroller.js
@@ -0,0 +1,72 @@
+import L from "leaflet";
+
+import { isArray } from "./util";
+import { Track } from "./track";
+
+/**
+ * 控制器类
+ * 控制轨迹和绘制
+ */
+export const TrackController = L.Class.extend({
+  initialize: function(tracks = [], draw, options) {
+    L.setOptions(this, options);
+
+    this._tracks = [];
+    this.addTrack(tracks);
+
+    this._draw = draw;
+
+    this._updateTime();
+  },
+
+  getMinTime: function() {
+    return this._minTime;
+  },
+
+  getMaxTime: function() {
+    return this._maxTime;
+  },
+
+  addTrack: function(track) {
+    if (isArray(track)) {
+      for (let i = 0, len = track.length; i < len; i++) {
+        this.addTrack(track[i]);
+      }
+    } else if (track instanceof Track) {
+      this._tracks.push(track);
+      this._updateTime();
+    } else {
+      throw new Error(
+        "tracks must be an instance of `Track` or an array of `Track` instance!"
+      );
+    }
+  },
+
+  drawTracksByTime: function(time) {
+    this._draw.clear();
+    for (let i = 0, len = this._tracks.length; i < len; i++) {
+      let track = this._tracks[i];
+      let tps = track.getTrackPointsBeforeTime(time);
+      if (tps && tps.length) this._draw.drawTrack(tps);
+    }
+  },
+
+  _updateTime: function() {
+    this._minTime = this._tracks[0].getStartTrackPoint().time;
+    this._maxTime = this._tracks[0].getEndTrackPoint().time;
+    for (let i = 0, len = this._tracks.length; i < len; i++) {
+      let stime = this._tracks[i].getStartTrackPoint().time;
+      let etime = this._tracks[i].getEndTrackPoint().time;
+      if (stime < this._minTime) {
+        this._minTime = stime;
+      }
+      if (etime > this._maxTime) {
+        this._maxTime = etime;
+      }
+    }
+  }
+});
+
+export const trackController = function(tracks, draw, options) {
+  return new TrackController(tracks, draw, options);
+};
diff --git a/src/track-playback/src/leaflet.trackplayback/tracklayer.js b/src/track-playback/src/leaflet.trackplayback/tracklayer.js
new file mode 100644
index 0000000000000000000000000000000000000000..08a21644a7ac2576a25a6c6fcd4650b61b8e576a
--- /dev/null
+++ b/src/track-playback/src/leaflet.trackplayback/tracklayer.js
@@ -0,0 +1,67 @@
+import L from "leaflet";
+
+/**
+ * 轨迹图层
+ */
+export const TrackLayer = L.Renderer.extend({
+  initialize: function(options) {
+    L.Renderer.prototype.initialize.call(this, options);
+    this.options.padding = 0.1;
+  },
+
+  onAdd: function(map) {
+    this._container = L.DomUtil.create("canvas", "leaflet-zoom-animated");
+
+    var pane = map.getPane(this.options.pane);
+    pane.appendChild(this._container);
+
+    this._ctx = this._container.getContext("2d");
+
+    this._update();
+  },
+
+  onRemove: function(map) {
+    L.DomUtil.remove(this._container);
+  },
+
+  getContainer: function() {
+    return this._container;
+  },
+
+  getBounds: function() {
+    return this._bounds;
+  },
+
+  _update: function() {
+    if (this._map._animatingZoom && this._bounds) {
+      return;
+    }
+    L.Renderer.prototype._update.call(this);
+
+    var b = this._bounds;
+
+    var container = this._container;
+
+    var size = b.getSize();
+
+    var m = L.Browser.retina ? 2 : 1;
+
+    L.DomUtil.setPosition(container, b.min);
+
+    // set canvas size (also clearing it); use double size on retina
+    container.width = m * size.x;
+    container.height = m * size.y;
+    container.style.width = size.x + "px";
+    container.style.height = size.y + "px";
+
+    if (L.Browser.retina) {
+      this._ctx.scale(2, 2);
+    }
+
+    // translate so we use the same path coordinates after canvas element moves
+    this._ctx.translate(-b.min.x, -b.min.y);
+
+    // Tell paths to redraw themselves
+    this.fire("update");
+  }
+});
diff --git a/src/track-playback/src/leaflet.trackplayback/trackplayback.js b/src/track-playback/src/leaflet.trackplayback/trackplayback.js
new file mode 100644
index 0000000000000000000000000000000000000000..460102ff1b594c01fad7396cb9517366a04242f8
--- /dev/null
+++ b/src/track-playback/src/leaflet.trackplayback/trackplayback.js
@@ -0,0 +1,131 @@
+import L from 'leaflet'
+
+import {
+  Track
+} from './track'
+import {
+  TrackController
+} from './trackcontroller'
+import {
+  Clock
+} from './clock'
+import {
+  Draw
+} from './draw'
+import * as Util from './util'
+
+/**
+ * single track data
+ * [{lat: 30, lng: 116, time: 1502529980, heading: 300, info:[]},{},....]
+ *
+ * mutiple track data
+ * [single track data, single track data, single track data]
+ */
+export const TrackPlayBack = L.Class.extend({
+
+  includes: L.Mixin.Events,
+
+  initialize: function (data, map, options = {}) {
+    let drawOptions = {
+      trackPointOptions: options.trackPointOptions,
+      trackLineOptions: options.trackLineOptions,
+      targetOptions: options.targetOptions,
+      toolTipOptions: options.toolTipOptions
+    }
+    this.tracks = this._initTracks(data)
+    this.draw = new Draw(map, drawOptions)
+    this.trackController = new TrackController(this.tracks, this.draw)
+    this.clock = new Clock(this.trackController, options.clockOptions)
+
+    this.clock.on('tick', this._tick, this)
+  },
+  start: function () {
+    this.clock.start()
+    return this
+  },
+  stop: function () {
+    this.clock.stop()
+    return this
+  },
+  rePlaying: function () {
+    this.clock.rePlaying()
+    return this
+  },
+  slowSpeed: function () {
+    this.clock.slowSpeed()
+    return this
+  },
+  quickSpeed: function () {
+    this.clock.quickSpeed()
+    return this
+  },
+  getSpeed: function () {
+    return this.clock.getSpeed()
+  },
+  getCurTime: function () {
+    return this.clock.getCurTime()
+  },
+  getStartTime: function () {
+    return this.clock.getStartTime()
+  },
+  getEndTime: function () {
+    return this.clock.getEndTime()
+  },
+  isPlaying: function () {
+    return this.clock.isPlaying()
+  },
+  setCursor: function (time) {
+    this.clock.setCursor(time)
+    return this
+  },
+  setSpeed: function (speed) {
+    this.clock.setSpeed(speed)
+    return this
+  },
+  showTrackPoint: function () {
+    this.draw.showTrackPoint()
+    return this
+  },
+  hideTrackPoint: function () {
+    this.draw.hideTrackPoint()
+    return this
+  },
+  showTrackLine: function () {
+    this.draw.showTrackLine()
+    return this
+  },
+  hideTrackLine: function () {
+    this.draw.hideTrackLine()
+    return this
+  },
+  dispose: function () {
+    this.clock.off('tick', this._tick)
+    this.draw.remove()
+    this.tracks = null
+    this.draw = null
+    this.trackController = null
+    this.clock = null
+  },
+  _tick: function (e) {
+    this.fire('tick', e)
+  },
+  _initTracks: function (data) {
+    let tracks = []
+    if (Util.isArray(data)) {
+      if (Util.isArray(data[0])) {
+        // 多条轨迹
+        for (let i = 0, len = data.length; i < len; i++) {
+          tracks.push(new Track(data[i]))
+        }
+      } else {
+        // 单条轨迹
+        tracks.push(new Track(data))
+      }
+    }
+    return tracks
+  }
+})
+
+export const trackplayback = function (data, map, options) {
+  return new TrackPlayBack(data, map, options)
+}
diff --git a/src/track-playback/src/leaflet.trackplayback/util.js b/src/track-playback/src/leaflet.trackplayback/util.js
new file mode 100644
index 0000000000000000000000000000000000000000..b40984d26d198f2dcb8dade0f18eba8153d44d87
--- /dev/null
+++ b/src/track-playback/src/leaflet.trackplayback/util.js
@@ -0,0 +1,3 @@
+export function isArray (arr) {
+  return Array.isArray ? Array.isArray(arr) : Object.prototype.toString.call(arr) === '[object Array]'
+}