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 5c9f07ee39e8b44af5cc40b69cfa789f956cafb9..c5f8a1782d6d1dacffb288c5afa5bd8b588c39dd 100644 --- a/src/App.css +++ b/src/App.css @@ -14,7 +14,7 @@ body { /* UserMap */ .map { position: absolute; - margin-top: 50px; + /* margin-top: 50px; */ height: 95vh; width: 100vw; } @@ -213,8 +213,14 @@ div.login button:hover { } .gamelist { + border: 2px solid white; + max-width: 800px; + max-height: 500px; + overflow: scroll; +} + +.gamelist-item { background-color: #1d1d1b; - max-width: 500px; padding: 10px; } @@ -234,3 +240,115 @@ div.login button:hover { display: flex; flex-direction: column; } + +.notification-popup { + position: absolute; + z-index: 1010; +} + +.notification-popup.warning { + background-color: yellow; + color: black; +} + +.notification-popup.alert { + background-color: red; +} + +.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; +} + +.filterContainer input:hover { + cursor: pointer; +} + +/* Score Counter */ +/* div.scoreContainer { + border-radius: 8px; + position: absolute; + display: flex; + flex-direction: row; + color: white; + background-color: #1d1d1b; + opacity: 0.9; + height: 50px; + max-width: 400px; + margin: auto; + top: 5px; + left: calc(50vw - 100px); + align-items: center; + justify-content: center; + z-index: 1000; +} + +div.scoreBlock { + flex: 1; + padding: 5px; + max-width: 120px; + text-align: center; + justify-content: center; + letter-spacing: 0.05em; + font-size: 1.5em; +} + +div.scoreDivider { + background-color: white; + float: left; + flex: 1; + width: 2px; + height: 70%; + margin: 5px; +} + +span.scoreCircle { + margin: 10px; + height: 25px; + width: 25px; + background-color: red; + border-radius: 50%; + display: inline-block; +} */ diff --git a/src/App.js b/src/App.js index 71bb3453a73f820887f93fbc1e734902789d9ac4..076ed8c301ea470b47b6e878b02e258b370d4c8d 100644 --- a/src/App.js +++ b/src/App.js @@ -11,6 +11,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 +125,11 @@ export default class App extends Component { /> ); }; + + replay = () => { + return <ReplayMap />; + }; + render() { // TODO: think better solution to wait for authenticator if (!this.state.authenticateComplete) { @@ -160,9 +166,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/DrawTools.js b/src/components/DrawTools.js index be1b7b35dedab72665c5c3221072c5781656b027..8040ce5920a3f990eebb6ce94081538103d2a6b7 100644 --- a/src/components/DrawTools.js +++ b/src/components/DrawTools.js @@ -246,51 +246,54 @@ class DrawTools extends Component { // The elements you draw will be added to this FeatureGroup layer, // when you hit edit button only items in this layer will be edited." <FeatureGroup> - <EditControl - position="topright" - onCreated={this._onCreated} - onEdited={this._onEdited} - onEditStart={this._onEditDeleteStart} - onEditStop={this._onEditDeleteStop} - onDeleted={this._onDeleted} - onDeleteStart={this._onEditDeleteStart} - onDeleteStop={this._onEditDeleteStop} - draw={{ - circle: { - repeatMode: false, // allows using the tool again after finishing the previous shape - shapeOptions: { - color: "#f9f10c", - opacity: 1 // affects the outline only. for some reason it wasn't at full opacity, so this is needed for more clarity - } - }, - rectangle: { - repeatMode: false - }, - polygon: { - repeatMode: true, - allowIntersection: false, // Restricts shapes to simple polygons - drawError: { - color: "#e1e100", // Color the shape will turn when intersects - message: "<strong>Oh snap!<strong> you can't draw that!" // Message that will show when intersect + {(this.props.role === "admin" || + this.props.role === "factionleader") && ( + <EditControl + position="topright" + onCreated={this._onCreated} + onEdited={this._onEdited} + onEditStart={this._onEditDeleteStart} + onEditStop={this._onEditDeleteStop} + onDeleted={this._onDeleted} + onDeleteStart={this._onEditDeleteStart} + onDeleteStop={this._onEditDeleteStop} + draw={{ + circle: { + repeatMode: false, // allows using the tool again after finishing the previous shape + shapeOptions: { + color: "#f9f10c", + opacity: 1 // affects the outline only. for some reason it wasn't at full opacity, so this is needed for more clarity + } }, - shapeOptions: { - color: "#ed2572", - opacity: 1 - } - }, - polyline: { - repeatMode: true, - shapeOptions: { - color: "#ed2572", - opacity: 1 - } - }, - marker: { - repeatMode: false - }, - circlemarker: false - }} - /> + rectangle: { + repeatMode: false + }, + polygon: { + repeatMode: true, + allowIntersection: false, // Restricts shapes to simple polygons + drawError: { + color: "#e1e100", // Color the shape will turn when intersects + message: "<strong>Oh snap!<strong> you can't draw that!" // Message that will show when intersect + }, + shapeOptions: { + color: "#ed2572", + opacity: 1 + } + }, + polyline: { + repeatMode: true, + shapeOptions: { + color: "#ed2572", + opacity: 1 + } + }, + marker: { + repeatMode: false + }, + circlemarker: false + }} + /> + )} {/* iterate through every element fetched from back-end */} {this.props.geoJSONLayer.features.map(feature => { diff --git a/src/components/EditGameForm.js b/src/components/EditGameForm.js index 2423ea337d3334f8b56db2b5efab876285d83bbc..1111a85d91291c5d593e60db96dece29add5c487 100644 --- a/src/components/EditGameForm.js +++ b/src/components/EditGameForm.js @@ -30,19 +30,14 @@ export default class EditGameForm extends React.Component { objectivePoints: [], capture_time: 300, confirmation_time: 60, - displayColorPicker: false + displayColorPicker: false, + saved: true }; - - this.handleMapDrag = this.handleMapDrag.bind(this); } - handleError = error => { - this.setState({ errorMsg: error }); - }; - handleChange = e => { const { name, value } = e.target; - this.setState({ [name]: value }); + this.setState({ [name]: value, saved: false }); }; handleFactionAdd = e => { @@ -185,7 +180,12 @@ export default class EditGameForm extends React.Component { // show/hide this form handleView = e => { - this.props.toggleView(this.props.view); + if ( + this.state.saved || + window.confirm("Are you sure you want to leave without saving?") + ) { + this.props.toggleView(this.props.view); + } }; // remove view with ESC @@ -195,17 +195,7 @@ export default class EditGameForm extends React.Component { } }; - handleMapDrag = e => { - this.setState({ - mapCenter: e.target.getCenter() - }); - }; - - handleMapScroll = e => { - this.setState({ - zoom: e.target.getZoom() - }); - }; + handleMapScroll = e => {}; handleGameSave = e => { e.preventDefault(); @@ -221,6 +211,8 @@ export default class EditGameForm extends React.Component { objectivePoints = []; } + console.log(objectivePoints); + // Object the form sends to server let gameObject = { name: this.state.gamename, @@ -230,24 +222,21 @@ export default class EditGameForm extends React.Component { enddate: endDate, center: this.state.mapCenter, factions: this.state.factions, - objective_points: objectivePoints - }; - - // Add node settings to the game if the game has objective points - if (objectivePoints.length > 0) { - gameObject.nodesettings = { + objective_points: objectivePoints, + nodesettings: { node_settings: { - capture_time: this.state.capture_time, - confirmation_time: this.state.confirmation_time, + capture_time: parseInt(this.state.capture_time), + confirmation_time: parseInt(this.state.confirmation_time), owner: 0, capture: 0, buttons_available: 16, heartbeat_interval: 60 } - }; - } + } + }; let token = sessionStorage.getItem("token"); + let error = false; // Send Game info to the server fetch(`${process.env.REACT_APP_API_URL}/game/edit/${this.props.gameId}`, { @@ -261,15 +250,23 @@ export default class EditGameForm extends React.Component { }) .then(res => { if (!res.ok) { - throw Error(res.statusMessage); - } else { - return res.json(); + error = true; } + return res.json(); }) .then(result => { alert(result.message); - this.props.onEditSave(); - this.handleView(); + if (!error) { + this.setState( + { + saved: true + }, + () => { + this.handleView(); + this.props.onEditSave(); + } + ); + } }) .catch(error => console.log("Error: ", error)); }; @@ -446,7 +443,7 @@ export default class EditGameForm extends React.Component { onSubmit={this.handleObjectivePointAdd} /> - <h1>Demo Game Editor</h1> + <h1>Game Editor</h1> <br /> <input placeholder="Game name" @@ -515,10 +512,10 @@ export default class EditGameForm extends React.Component { /> <br /> <br /> - <label>Factions</label> <br /> <input + id="editGameFactionNameInput" name="factionNameInput" id="factionNameInput" value={this.state.factionNameInput} @@ -528,15 +525,18 @@ export default class EditGameForm extends React.Component { form="factionAddFrom" /> <input + id="editGameFactionPasswordInput" name="factionPasswordInput" id="factionPasswordInput" value={this.state.factionPasswordInput} minLength="3" onChange={this.handleChange} placeholder="Faction password" + type="password" form="factionAddFrom" /> <div + id="editGameColorPickerButton" style={styles.swatch} onClick={() => this.setState({ @@ -548,6 +548,7 @@ export default class EditGameForm extends React.Component { </div> {this.state.displayColorPicker && ( <div + id="editGameColorPicker" style={styles.cover} onClick={() => this.setState({ displayColorPicker: false })} > @@ -559,7 +560,11 @@ export default class EditGameForm extends React.Component { /> </div> )} - <button type="submit" form="factionAddFrom" id="factionAddButton"> + <button + id="editGameFactionSubmitButton" + type="submit" + form="factionAddFrom" + > Add </button> <ul>{factions}</ul> @@ -568,6 +573,7 @@ export default class EditGameForm extends React.Component { <label>Objective points</label> <br /> <input + id="editGameObjectivePointDescriptionInput" name="objectivePointDescriptionInput" id="objectivePointDescriptionInput" type="number" @@ -578,6 +584,7 @@ export default class EditGameForm extends React.Component { form="objectivePointAddFrom" /> <input + id="editGameObjectivePointMultiplierInput" name="objectivePointMultiplierInput" id="objectivePointMultiplierInput" type="number" @@ -586,7 +593,11 @@ export default class EditGameForm extends React.Component { placeholder="Objective point multiplier" form="objectivePointAddFrom" /> - <button type="submit" form="objectivePointAddFrom" id="objectivePointAddButton"> + <button + id="editGameObjectivePointSubmitButton" + type="submit" + form="objectivePointAddFrom" + > Add </button> <ul>{objectivePoints}</ul> @@ -599,6 +610,7 @@ export default class EditGameForm extends React.Component { Capture time: </label> <input + id="editGameCaptureTimeInput" name="capture_time" id="captureTimeInput" type="number" @@ -608,6 +620,7 @@ export default class EditGameForm extends React.Component { /> <label className="">Confimation time:</label> <input + id="editGameConfirmationTimeInput" name="confirmation_time" id="confirmationTimeInput" type="number" @@ -626,8 +639,8 @@ export default class EditGameForm extends React.Component { zoom={this.state.zoom} maxZoom="13" style={{ height: "400px", width: "400px" }} - onmoveend={this.handleMapDrag} - onzoomend={this.handleMapScroll} + onmoveend={e => this.setState({ mapCenter: e.target.getCenter() })} + onzoomend={e => this.setState({ zoom: e.target.getZoom() })} > <TileLayer attribution="Maanmittauslaitoksen kartta" @@ -636,7 +649,7 @@ export default class EditGameForm extends React.Component { </Map> <br /> <button - id="gameDeleteButton" + id="editGameDeleteGameButton" style={{ backgroundColor: "red" }} type="submit" form="gameDeletionForm" diff --git a/src/components/GameCard.js b/src/components/GameCard.js index bfa10a6097d919d4339e6341e99e753b4d41b202..18a1e67a7a5287cffa1d189caf2b8d926d171b64 100644 --- a/src/components/GameCard.js +++ b/src/components/GameCard.js @@ -58,6 +58,13 @@ export default class GameCard extends React.Component { Select </button> </Link> + <Link + to={{ pathname: "/replay", search: "?id=" + this.state.gameInfo.id }} + > + <button id={`replay${this.state.gameInfo.name}`} type="button"> + Replay + </button> + </Link> </div> ); } diff --git a/src/components/GameInfoView.js b/src/components/GameInfoView.js new file mode 100644 index 0000000000000000000000000000000000000000..dc961d61a855e0bccf28c2a5a82c335817de66a4 --- /dev/null +++ b/src/components/GameInfoView.js @@ -0,0 +1,70 @@ +import React from "react"; +import { Map, TileLayer } from "react-leaflet"; + +export default class GameInfoView extends React.Component { + componentDidMount() { + document.addEventListener("keyup", this.handleEsc); + } + + componentWillUnmount() { + document.removeEventListener("keyup", this.handleEsc); + } + + handleEsc = e => { + if (e.keyCode === 27) { + this.props.toggleView(); + } + }; + + render() { + if (this.props.gameInfo === undefined) { + return false; + } + return ( + <div className="fade-main"> + <div className="sticky"> + <span + id="closeGameInfoX" + className="close" + onClick={this.props.toggleView} + > + × + </span> + </div> + <div className=""> + <h1>Game Info</h1> + <p>Game name: {this.props.gameInfo.name}</p> + <p>Description: {this.props.gameInfo.desc}</p> + <p>Start date: {this.props.gameInfo.startdate}</p> + <p>End date: {this.props.gameInfo.enddate}</p> + <h2>Factions</h2> + <div> + {this.props.gameInfo.factions.map(faction => ( + <p key={faction.factionId} style={{ color: faction.colour }}> + {faction.factionName} + </p> + ))} + </div> + <div> + <Map + id="gameInfoCenterMap" + className="" + center={[ + this.props.gameInfo.center.lat, + this.props.gameInfo.center.lng + ]} + zoom="13" + maxZoom="13" + style={{ height: "400px", width: "400px" }} + > + <TileLayer + attribution="Maanmittauslaitoksen kartta" + url=" https://tiles.kartat.kapsi.fi/taustakartta/{z}/{x}/{y}.jpg" + /> + </Map> + </div> + </div> + </div> + ); + } +} diff --git a/src/components/GameList.js b/src/components/GameList.js index fcb3091c15867045b985157192b864789dabc204..c7df36e8ae8147d2e051ac1976101689f208abee 100644 --- a/src/components/GameList.js +++ b/src/components/GameList.js @@ -35,8 +35,6 @@ class GameList extends React.Component { ? games[0].id : undefined }); - // taking the initialized gameID to App.js (GameList.js -> GameSidebar.js -> Header.js -> App.js) - this.props.handleGameChange(games[0].id); }) .catch(error => { console.log(error); @@ -80,20 +78,18 @@ class GameList extends React.Component { }; render() { - let gamelistItems = this.props.games.map(game => { - return ( - <GameCard - key={game.id} - gameId={game.id} - onEditSave={this.props.onEditSave} - /> - ); - }); + let gamelistItems = this.props.games.map(game => ( + <GameCard + key={game.id} + gameId={game.id} + onEditSave={this.props.onEditSave} + /> + )); return ( - <Fragment> - <div className="gamelist">{gamelistItems}</div> - </Fragment> + <div className="gamelist"> + <div className="gamelist-item">{gamelistItems}</div> + </div> ); } } diff --git a/src/components/GameStateButtons.js b/src/components/GameStateButtons.js index c770ed7896facdddaf56adecde4c689d10dbeb3f..946f9e5b14003d7ad793f08bc9a8f67da8196dea 100644 --- a/src/components/GameStateButtons.js +++ b/src/components/GameStateButtons.js @@ -6,53 +6,71 @@ export default class GameStateButtons extends React.Component { }; setGameState(state) { - console.log(state); - let token = sessionStorage.getItem("token"); - let error = false; - fetch( - `${process.env.REACT_APP_API_URL}/game/edit-state/${this.props.gameId}`, - { - method: "PUT", - headers: { - Authorization: "Bearer " + token, - Accept: "application/json", - "Content-Type": "application/json" - }, - body: JSON.stringify({ - id: this.props.gameId, - state: state - }) - } - ) - .then(res => { - if (!res.ok) { - error = true; - } - return res.json(); - }) - .then(res => { - if (error) { - console.log(res); - } else { - alert(`Game state changed to ${state}`); - this.setState({ gameState: state }); + if ( + window.confirm(`Are you sure you want to change game state to ${state}?`) + ) { + let token = sessionStorage.getItem("token"); + let error = false; + fetch( + `${process.env.REACT_APP_API_URL}/game/edit-state/${this.props.gameId}`, + { + method: "PUT", + headers: { + Authorization: "Bearer " + token, + Accept: "application/json", + "Content-Type": "application/json" + }, + body: JSON.stringify({ + id: this.props.gameId, + state: state + }) } - }) - .catch(error => console.log(error)); + ) + .then(res => { + if (!res.ok) { + error = true; + } + return res.json(); + }) + .then(res => { + if (error) { + console.log(res); + } else { + alert(`Game state changed to ${state}`); + this.setState({ gameState: state }); + } + }) + .catch(error => console.log(error)); + } } render() { if (this.state.gameState === "CREATED") { return ( - <button onClick={() => this.setGameState("STARTED")}>Start</button> + <button + id="gameStateStartButton" + onClick={() => this.setGameState("STARTED")} + > + Start + </button> ); } if (this.state.gameState === "STARTED") { return ( <Fragment> - <button onClick={() => this.setGameState("PAUSED")}>Pause</button> - <button onClick={() => this.setGameState("ENDED")}>Stop</button> + <button + id="gameStatePauseButton" + onClick={() => this.setGameState("PAUSED")} + > + Pause + </button> + <button + id="gameStateStopButton" + onClick={() => this.setGameState("ENDED")} + > + Stop + </button> </Fragment> ); } @@ -60,8 +78,18 @@ export default class GameStateButtons extends React.Component { if (this.state.gameState === "PAUSED") { return ( <Fragment> - <button onClick={() => this.setGameState("STARTED")}>Continue</button> - <button onClick={() => this.setGameState("ENDED")}>Stop</button> + <button + id="gameStateContinueButton" + onClick={() => this.setGameState("STARTED")} + > + Continue + </button> + <button + id="gameStateStopButton" + onClick={() => this.setGameState("ENDED")} + > + Stop + </button> </Fragment> ); } diff --git a/src/components/GameView.js b/src/components/GameView.js index 32474d7a8c4e6c520fd69fc468156cbfc1c567cf..2215bf242a3920b0c208e8ee0bdf770cdc202c0e 100644 --- a/src/components/GameView.js +++ b/src/components/GameView.js @@ -8,6 +8,8 @@ import PlayerlistView from "./PlayerlistView"; import NotificationView from "./NotificationView"; import GameStateButtons from "./GameStateButtons"; import ClientSocket from "./Socket"; +import NotificationPopup from "./NotificationPopup"; +import GameInfoView from "./GameInfoView"; export default class GameView extends React.Component { state = { @@ -18,35 +20,19 @@ export default class GameView extends React.Component { lng: 25.7597186, zoom: 13, mapUrl: "https://tiles.kartat.kapsi.fi/taustakartta/{z}/{x}/{y}.jpg", - socketSignal: null + socketSignal: null, + socket: null }; componentDidMount() { let gameId = new URL(window.location.href).searchParams.get("id"); - let token = sessionStorage.getItem("token"); - - fetch(`${process.env.REACT_APP_API_URL}/game/${gameId}`) - .then(res => { - if (!res.ok) { - throw Error(); - } - }) - .catch(error => { - alert("Game not found"); - window.document.location.href = "/"; - }); + this.getGameInfo(gameId); + this.getPlayerRole(gameId); + } - // Get game info - fetch(`${process.env.REACT_APP_API_URL}/game/${gameId}`) - .then(res => res.json()) - .then(res => { - this.setState({ - gameInfo: res - }); - }) - .catch(error => console.log(error)); + getPlayerRole(gameId) { + let token = sessionStorage.getItem("token"); - // Get Role fetch(`${process.env.REACT_APP_API_URL}/faction/check-faction/${gameId}`, { method: "GET", headers: { @@ -60,6 +46,26 @@ export default class GameView extends React.Component { .catch(error => console.log(error)); } + getGameInfo(gameId) { + fetch(`${process.env.REACT_APP_API_URL}/game/${gameId}`) + .then(res => { + if (!res.ok) { + throw Error(); + } else { + return res.json(); + } + }) + .then(res => { + this.setState({ + gameInfo: res + }); + }) + .catch(error => { + alert("Game not found"); + window.document.location.href = "/"; + }); + } + handleLeaveFaction = e => { let token = sessionStorage.getItem("token"); let error = false; @@ -76,23 +82,54 @@ export default class GameView extends React.Component { ) .then(res => { if (!res.ok) { - error = true; + throw Error(); + } else { + return res.json(); } - return res.json(); }) .then(res => { alert(res.message); + this.getPlayerRole(this.state.gameInfo.id); }) - .catch(error => console.log(error)); + .catch(error => { + alert("Game not found"); + window.document.location.href = "/"; + }); + }; + + handleLeaveFaction = e => { + if (window.confirm("Are you sure you want to leave your faction?")) { + let token = sessionStorage.getItem("token"); + fetch( + `${process.env.REACT_APP_API_URL}/faction/leave/${ + this.state.gameInfo.id + }`, + { + method: "DELETE", + headers: { + Authorization: "Bearer " + token + } + } + ) + .then(res => { + if (!res.ok) { + } + return res.json(); + }) + .then(res => { + alert(res.message); + this.getPlayerRole(this.state.gameInfo.id); + }) + .catch(error => console.log(error)); + } }; // setting the socket signal automatically fires shouldComponentUpdate function where socketSignal prop is present // setting socketSignal to null immediately after to avoid multiple database fetches - getSocketSignal = type => { - console.log(type); + getSocketSignal = data => { this.setState( { - socketSignal: type + socketSignal: data }, () => { this.setState({ @@ -102,13 +139,21 @@ export default class GameView extends React.Component { ); }; - render() { - const initialPosition = [this.state.lat, this.state.lng]; + onSocketChange = newSocket => { + this.setState({ + socket: newSocket + }); + }; + render() { + console.log(this.state.gameInfo); + const initialPosition = this.state.gameInfo + ? [this.state.gameInfo.center.lat, this.state.gameInfo.center.lng] + : null; return ( <div> <Link to="/"> - <button id="selectGameButton">Game selection</button> + <button id="gameViewGameSelectionButton">Game selection</button> </Link> {this.state.gameInfo !== null && ( <div> @@ -116,6 +161,7 @@ export default class GameView extends React.Component { <ClientSocket gameId={this.state.gameInfo.id} getSocketSignal={this.getSocketSignal} + onSocketChange={this.onSocketChange} /> )} <div>Game Name: {this.state.gameInfo.name}</div> @@ -125,14 +171,21 @@ export default class GameView extends React.Component { {this.state.role !== "" && ( <div>Your role in this game: {this.state.role}</div> )} - {this.state.role === "admin" && ( - <button - id="editGameButton" - onClick={() => this.setState({ form: "edit" })} - > - Edit - </button> - )} + {this.state.role === "admin" && + this.state.gameInfo.state === "CREATED" && ( + <button + id="editGameButton" + onClick={() => this.setState({ form: "edit" })} + > + Edit + </button> + )} + <button + id="gameInfoButton" + onClick={() => this.setState({ form: "info" })} + > + Game Info + </button> {this.state.role === "" && ( <button id="joinGameButton" @@ -159,6 +212,7 @@ export default class GameView extends React.Component { <TaskListButton gameId={this.state.gameInfo.id} role={this.state.role} + factions={this.state.gameInfo.factions} /> )} {this.state.role !== "admin" && this.state.role !== "" && ( @@ -172,27 +226,34 @@ export default class GameView extends React.Component { gameId={this.state.gameInfo.id} /> )} - <UserMap - position={initialPosition} - zoom={this.state.zoom} - mapUrl={this.state.mapUrl} - currentGameId={this.state.gameInfo.id} - socketSignal={this.state.socketSignal} - /> + {initialPosition && ( + <UserMap + position={initialPosition} + zoom={this.state.zoom} + mapUrl={this.state.mapUrl} + currentGameId={this.state.gameInfo.id} + socketSignal={ + this.state.socketSignal === null + ? null + : this.state.socketSignal.type + } + role={this.state.role} + > + <NotificationPopup socketSignal={this.state.socketSignal} /> + </UserMap> + )} {this.state.form === "edit" && ( <EditGameForm gameId={this.state.gameInfo.id} toggleView={() => this.setState({ form: "" })} - onEditSave={() => { - this.getGameInfo(); - }} + onEditSave={() => this.getGameInfo(this.state.gameInfo.id)} /> )} {this.state.form === "join" && ( <JoinGameForm gameId={this.state.gameInfo.id} toggleView={() => this.setState({ form: "" })} - onJoin={() => console.log("joinde")} + onJoin={() => this.getPlayerRole(this.state.gameInfo.id)} /> )} {this.state.form === "players" && ( @@ -206,6 +267,19 @@ export default class GameView extends React.Component { <NotificationView gameId={this.state.gameInfo.id} toggleView={() => this.setState({ form: "" })} + socket={this.state.socket} + role={this.state.role} + gameState={ + this.state.gameInfo !== undefined + ? this.state.gameInfo.state + : "" + } + /> + )} + {this.state.form === "info" && ( + <GameInfoView + gameInfo={this.state.gameInfo} + toggleView={() => this.setState({ form: "" })} /> )} </div> diff --git a/src/components/JoinGameForm.js b/src/components/JoinGameForm.js index 3b8d97fd0b60ad09c727d30a79561a4abc74f29d..c2a43d7399cf4e4b1e6a6dbac7f29143aa46be89 100644 --- a/src/components/JoinGameForm.js +++ b/src/components/JoinGameForm.js @@ -14,7 +14,7 @@ export default class JoinGameForm extends React.Component { } // Get game info - //TODO: from props + //TODO: gameinfo from props componentDidMount() { if (this.props.gameId === undefined) { alert("game not selected"); @@ -122,6 +122,7 @@ export default class JoinGameForm extends React.Component { onChange={e => this.setState({ factionPasswordInput: e.target.value }) } + type="password" placeholder="Password" minLength="3" required diff --git a/src/components/LoginForm.js b/src/components/LoginForm.js index f7154f038be5f5ccdf6852f1ed242276d0b7ca4d..d1e90cb994a8abfd30e15ad81aaaac2ac6c64ad9 100644 --- a/src/components/LoginForm.js +++ b/src/components/LoginForm.js @@ -87,7 +87,7 @@ export class LoginForm extends React.Component { <h2>{this.state.errorMsg}</h2> </form> <Link to="/register"> - <button id="openRegisterFormButton">Create account</button> + <button id="loginRegisterButton">Create account</button> </Link> </div> </div> diff --git a/src/components/NotificationCard.js b/src/components/NotificationCard.js new file mode 100644 index 0000000000000000000000000000000000000000..cf49d28f285a8f6f662007b947b1d7a42cc682fc --- /dev/null +++ b/src/components/NotificationCard.js @@ -0,0 +1,11 @@ +import React from "react"; + +export default class NotificationCard extends React.Component { + render() { + return ( + <div> + {this.props.notification.type} : {this.props.notification.message} + </div> + ); + } +} diff --git a/src/components/NotificationPopup.js b/src/components/NotificationPopup.js new file mode 100644 index 0000000000000000000000000000000000000000..a3e9a723a6b62f03d7ace1024c1912e8f32ee38a --- /dev/null +++ b/src/components/NotificationPopup.js @@ -0,0 +1,58 @@ +import React from "react"; + +export default class NotificationPopup extends React.Component { + state = { + lastNotification: null, + visible: true + }; + + componentDidUpdate(prevProps, prevState) { + if ( + prevProps.socketSignal !== null && + prevProps.socketSignal !== this.state.lastNotification + ) { + if (prevProps.socketSignal.type === "alert") { + this.setState({ + lastNotification: prevProps.socketSignal, + visible: true + }); + } + if (prevProps.socketSignal.type === "note") { + this.setState({ + lastNotification: prevProps.socketSignal, + visible: true + }); + } + } + } + + render() { + if (this.state.lastNotification !== null && this.state.visible) { + return ( + <div + className={ + this.state.lastNotification.type === "alert" + ? "notification-popup alert" + : "notification-popup warning" + } + > + <button + id="NotificationPopupCloseButton" + onClick={() => { + this.setState({ visible: false }); + }} + > + Close + </button> + <br /> + <label> + {this.state.lastNotification.type === "alert" ? "ALERT" : "Note"} + </label> + <p>{this.state.lastNotification.message}</p> + </div> + ); + } + + return false; + } +} diff --git a/src/components/NotificationView.js b/src/components/NotificationView.js index 5fcf80141e569562492417d719854bd7b654ced1..d63d0253b0f585b9ad7d904fb9eaced0fa28b3d9 100644 --- a/src/components/NotificationView.js +++ b/src/components/NotificationView.js @@ -1,7 +1,99 @@ import React from "react"; +import NotificationCard from "./NotificationCard"; export default class NotificationView extends React.Component { + state = { + notifications: [], + notificationInput: "", + notificationTypeInput: "note" + }; + + componentDidMount() { + this.getNotifications(this.props.gameId); + } + + getNotifications(gameId) { + let token = sessionStorage.getItem("token"); + fetch(`${process.env.REACT_APP_API_URL}/notifications/${gameId}`, { + headers: { + Authorization: "Bearer " + token + } + }) + .then(res => res.json()) + .then(res => { + this.setState({ notifications: res.reverse() }); + }); + } + + handleSend = e => { + e.preventDefault(); + + if (this.state.notificationInput === "") { + alert("notification message can't be empty"); + } else if ( + window.confirm("Are you sure you want to send the notification") + ) { + this.props.socket.emit("game-info", { + type: this.state.notificationTypeInput, + message: this.state.notificationInput, + game: this.props.gameId + }); + alert("Notification sent"); + this.getNotifications(this.props.gameId); + this.setState({ notificationInput: "" }); + } + }; + render() { - return false; + let notifications = this.state.notifications.map(notification => ( + <NotificationCard key={notification.id} notification={notification} /> + )); + + return ( + <div className="fade-main"> + <button + id="notificationViewCloseButton" + onClick={() => this.props.toggleView()} + > + Close + </button> + <div> + {this.props.role === "admin" && + this.props.gameState !== "ENDED" && + this.props.gameState !== "CREATED" && ( + <form onSubmit={this.handleSend}> + <select + id="notificationViewTypeSelect" + value={this.state.notificationTypeInput} + onChange={e => + this.setState({ notificationTypeInput: e.target.value }) + } + > + <option value="note">Note</option> + <option value="alert">Alert</option> + </select> + <input + id="notificationViewMessageInput" + type="text" + value={this.state.notificationInput} + onChange={e => + this.setState({ notificationInput: e.target.value }) + } + placeholder="Notification message..." + /> + <button id="notificationSubmitButton" type="submit"> + Send Notification + </button> + </form> + )} + {this.props.role === "admin" && + (this.props.gameState === "ENDED" || + this.props.gameState === "CREATED") && ( + <p>Notifications can only be sent if the game is ongoing</p> + )} + </div> + {notifications} + </div> + ); } } diff --git a/src/components/PlayerlistPlayerCard.js b/src/components/PlayerlistPlayerCard.js index ebc2ddeb5c6eb75958cf6390404206f488f73878..7260d5abaa4b4f617cb32ea7b428756f5ab606ba 100644 --- a/src/components/PlayerlistPlayerCard.js +++ b/src/components/PlayerlistPlayerCard.js @@ -69,13 +69,20 @@ export default class PlayerlistPlayerCard extends React.Component { <div> {this.props.player.person.name} :{" "} <select + id={"playerCardRoleSelect" + this.props.player.person.name} value={this.state.roleInput} onChange={e => this.setState({ roleInput: e.target.value })} > {roleOptions()} </select> - <button onClick={this.handleSave}>Save</button> <button + id={"playerCardSaveButton" + this.props.player.person.name} + onClick={this.handleSave} + > + Save + </button> + <button + id={"playerCardCancelButton" + this.props.player.person.name} onClick={() => { this.setState({ edit: false, roleInput: this.props.player.role }); }} @@ -92,16 +99,13 @@ export default class PlayerlistPlayerCard extends React.Component { {this.props.player.person.name} : {this.state.edit && roleOptions()} {!this.state.edit && this.props.player.role} {this.props.role === "admin" && !this.state.edit && ( - <button onClick={() => this.setState({ edit: !this.state.edit })}> + <button + id={"playerCardEditButton" + this.props.player.person.name} + onClick={() => this.setState({ edit: !this.state.edit })} + > Edit </button> )} - {this.state.edit && ( - <Fragment> - <button>Save</button> - <button>Cancel</button> - </Fragment> - )} </div> ); } diff --git a/src/components/PlayerlistView.js b/src/components/PlayerlistView.js index 6967b0e2e0dc25017ff4940710e3d1e4488a6150..60c294fc257c3f12cd9fded17659c8e0323be4c5 100644 --- a/src/components/PlayerlistView.js +++ b/src/components/PlayerlistView.js @@ -71,7 +71,7 @@ export default class PlayerlistView extends React.Component { <div className="fade-main"> <div className="sticky"> <span - id="closeEditGameFormX" + id="closePlayerlistX" className="close" onClick={() => this.props.toggleView()} > diff --git a/src/components/RegisterForm.js b/src/components/RegisterForm.js index 7bbb278999bc691757b769bf4dbb7dd41b9c22f0..a28c4502bec30600a1f15c91ba7f346168b1d7d4 100644 --- a/src/components/RegisterForm.js +++ b/src/components/RegisterForm.js @@ -72,6 +72,7 @@ export class RegisterForm extends React.Component { <h1>Register</h1> <br /> <input + id="registerUsernameInput" placeholder="Enter Username" name="username" value={this.state.username} @@ -82,6 +83,7 @@ export class RegisterForm extends React.Component { /> <br /> <input + id="registerPasswordInput" placeholder="Enter password" type="password" name="password" @@ -91,6 +93,7 @@ export class RegisterForm extends React.Component { /> <br /> <input + id="registerPasswordVerifyInput" placeholder="Verify password" type="password" name="password2" diff --git a/src/components/ReplayConfig.js b/src/components/ReplayConfig.js new file mode 100644 index 0000000000000000000000000000000000000000..9eea9de7fcf39dd7eea0bce866e486632b7866ef --- /dev/null +++ b/src/components/ReplayConfig.js @@ -0,0 +1,45 @@ +import React from "react"; + +var options = { + 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 + } +}; + +export default options; diff --git a/src/components/ReplayMap.js b/src/components/ReplayMap.js new file mode 100644 index 0000000000000000000000000000000000000000..ec30ce883a4ef3fdcbed4bfc925c2f0d2951e63a --- /dev/null +++ b/src/components/ReplayMap.js @@ -0,0 +1,93 @@ +// https://github.com/linghuam/Leaflet.TrackPlayBack + +import React from "react"; +import { Link } from "react-router-dom"; +import L from "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 options from "./ReplayConfig"; + +export default class ReplayMap extends React.Component { + constructor(props) { + super(props); + this.state = { + // stores game's initial location + location: [62.3, 25.7], + // stores player locations from backend + players: [], + // stores all factions from the game + factions: [], + // stores all scores from the game + scores: [], + // stores all drawings from backend + drawings: [] + }; + } + + async componentDidMount() { + // set gameId to state from URL + await this.setState({ + gameId: await new URL(window.location.href).searchParams.get("id") + }); + // fetch all data and set it to state + let replaydata = await this.fetchReplayData(); + await this.setState({ + location: replaydata.location, + players: replaydata.players, + factions: replaydata.factions, + scores: replaydata.scores, + drawings: replaydata.drawings + }); + replaydata ? this.replay() : alert("No replay data was found"); + } + + fetchReplayData = async () => { + let res = await fetch( + `${process.env.REACT_APP_API_URL}/replay/${this.state.gameId}` + ); + if (await res.ok) { + return await res.json(); + } else { + alert("Game not found"); + window.document.location.href = "/"; + } + }; + + tickDrawings = () => { + return this.state.allGeoJSON.map(drawing => { + return drawing[0]; + }); + }; + + replay = () => { + // create a map for the replay, setView to game's center cords + this.map = L.map(this.refs.map).setView( + this.state.location || [62.3, 25.7], + 14 + ); + L.tileLayer("https://tiles.kartat.kapsi.fi/taustakartta/{z}/{x}/{y}.jpg", { + attribution: + '© <a href="https://www.maanmittauslaitos.fi/">Maanmittauslaitos</a>' + }).addTo(this.map); + // import options from ReplayConfig.js + this.trackplayback = new L.TrackPlayBack(this.state, this.map, options); + this.trackplaybackControl = L.trackplaybackcontrol( + this.trackplayback, + this.map + ); + this.trackplaybackControl.addTo(this.map); + }; + + render() { + return ( + <React.Fragment> + <Link to="/"> + <button>Game selection</button> + </Link> + <div className="map" ref="map" /> + </React.Fragment> + ); + } +} diff --git a/src/components/Socket.js b/src/components/Socket.js index 06cc2fd899f90ce897e5bbecdd598410aca02cb1..07142e51ea875131ffa3605ce2f83fcee746fd01 100644 --- a/src/components/Socket.js +++ b/src/components/Socket.js @@ -20,8 +20,8 @@ export default class ClientSocket extends React.Component { console.log("hi socket"); // need to explicitly update drawings and trackings when gameID first becomes available if (this.props.gameId !== null) { - await this.props.getSocketSignal("drawing-update"); - await this.props.getSocketSignal("tracking-update"); + await this.props.getSocketSignal({ type: "drawing-update" }); + await this.props.getSocketSignal({ type: "tracking-update" }); } this.initSocket(); } @@ -38,7 +38,7 @@ export default class ClientSocket extends React.Component { // disconnect the socket on component dismount componentWillUnmount() { - console.log("bye socket"); + // console.log("bye socket"); this.state.sock.disconnect(); } @@ -47,12 +47,12 @@ export default class ClientSocket extends React.Component { // set the socket to listen gameId-thread socket.on(this.props.gameId, data => { - console.log(data); - this.props.getSocketSignal(data.type); + this.props.getSocketSignal(data); // check socket update type - this.setState({ update: data.type }); + this.setState({ update: data }); }); + this.props.onSocketChange(socket); this.setState({ sock: socket }); }; diff --git a/src/components/TaskItem.js b/src/components/TaskItem.js index b9dca50947d36503a5457c9310546180bb8769fe..d4b91559d878b513d2e4b2261653fd46e4a9d488 100644 --- a/src/components/TaskItem.js +++ b/src/components/TaskItem.js @@ -14,12 +14,6 @@ class TaskItem extends React.Component { this.getFactionlist(this.props.gameId); } - onEditClick = e => { - this.setState({ - edit: !this.state.edit - }); - }; - getFactionlist(gameId) { fetch(`${process.env.REACT_APP_API_URL}/game/${gameId}`, { method: "GET" @@ -70,15 +64,11 @@ class TaskItem extends React.Component { }; render() { - let factionlistItems = []; - for (let i = 0; i < this.state.factions.length; i++) { - const faction = this.state.factions[i]; - factionlistItems.push( - <option key={faction.factionId} value={faction.factionId}> - {faction.factionName} - </option> - ); - } + let factionlistItems = this.state.factions.map(faction => ( + <option key={faction.factionId} value={faction.factionId}> + {faction.factionName} + </option> + )); return ( <div className="tasklist-item"> @@ -100,11 +90,17 @@ class TaskItem extends React.Component { )} </div> {this.props.task.taskIsActive && this.props.role === "admin" && ( - <button onClick={this.onEditClick}>Edit</button> + <button + id={"taskEditButton" + this.props.task.taskName} + onClick={() => this.setState({ edit: !this.state.edit })} + > + {this.state.edit ? "Cancel" : "Set Winner"} + </button> )} {this.state.edit && ( <form onSubmit={this.onSaveSubmit}> <select + id={"taskWinnerSelect" + this.props.task.taskName} value={this.state.selectedFactionId} onChange={e => this.setState({ selectedFactionId: e.target.value }) @@ -117,6 +113,7 @@ class TaskItem extends React.Component { )} {this.props.role === "admin" && ( <button + id={"taskDeleteButton" + this.props.task.taskName} onClick={this.onTaskDelete} style={{ backgroundColor: "red" }} > diff --git a/src/components/TaskList.js b/src/components/TaskList.js index ce08afc64a2f4ccfab6d1d77cd0f8594e1cce963..1b8e1374f85cb8c81b6212368e370e9c1495c3cf 100644 --- a/src/components/TaskList.js +++ b/src/components/TaskList.js @@ -6,17 +6,15 @@ class TaskList extends React.Component { constructor(props) { super(props); this.state = { - taskNameInput: "", // >= 3 - taskDescriptionInput: "", // no limits + taskNameInput: "", // 3-31 chars + taskDescriptionInput: "", // 0-255 tasks: [], - factionlist: [], selectedFactionId: "" }; } componentDidMount() { this.getTasks(this.props.gameId); - this.getFactionlist(this.props.gameId); // TODO: remove if the user is not admin? } getTasks(gameId) { @@ -42,25 +40,6 @@ class TaskList extends React.Component { .catch(error => console.log(error)); } - getFactionlist(gameId) { - fetch(`${process.env.REACT_APP_API_URL}/game/${gameId}`, { - method: "GET" - }) - .then(result => { - if (!result.ok) { - throw Error(result.responseText); - } else { - return result.json(); - } - }) - .then(result => { - this.setState({ - factionlist: result.factions - }); - }) - .catch(error => console.log(error)); - } - handleTaskCreation = e => { e.preventDefault(); if (this.state.taskNameInput === "") { @@ -206,10 +185,10 @@ class TaskList extends React.Component { } } - let factionlistItems = this.state.factionlist.map(item => { + let factionlistItems = this.props.factions.map(faction => { return ( - <option key={item.factionId} value={item.factionId}> - {item.factionName} + <option key={faction.factionId} value={faction.factionId}> + {faction.factionName} </option> ); }); @@ -232,6 +211,7 @@ class TaskList extends React.Component { type="text" placeholder="Task name" minLength="3" + maxLength="31" value={this.state.taskNameInput} onChange={e => this.setState({ taskNameInput: e.target.value })} /> @@ -239,6 +219,7 @@ class TaskList extends React.Component { id="taskDescriptionInput" placeholder="Task description" value={this.state.taskDescriptionInput} + maxLength="255" onChange={e => this.setState({ taskDescriptionInput: e.target.value }) } diff --git a/src/components/TaskListButton.js b/src/components/TaskListButton.js index c0e5f8c1257edaaae2e062fbc8dce562538aa5c1..bb56314129f92204ab45d226b3d0f69593b18d05 100644 --- a/src/components/TaskListButton.js +++ b/src/components/TaskListButton.js @@ -45,7 +45,11 @@ export default class TaskListButton extends React.Component { Tasks </button> {this.state.open && ( - <TaskList gameId={this.props.gameId} role={this.props.role} /> + <TaskList + gameId={this.props.gameId} + role={this.props.role} + factions={this.props.factions} + /> )} </Fragment> ); diff --git a/src/components/UserMap.js b/src/components/UserMap.js index c5347fa78df07b4092296702fae1e70ee1704f8f..83f4de8d09da8efa58426855273b27fbf4145b0b 100644 --- a/src/components/UserMap.js +++ b/src/components/UserMap.js @@ -39,6 +39,7 @@ class UserMap extends Component { } componentDidUpdate(prevProps, prevState) { + console.log(prevProps.socketSignal); if (prevProps.socketSignal === "drawing-update") { this.fetchGeoJSON(); } @@ -167,6 +168,7 @@ class UserMap extends Component { sendGeoJSON={this.sendGeoJSON} geoJSONLayer={this.state.geoJSONLayer} currentGameId={this.props.currentGameId} + role={this.props.role} /> {this.state.ownLat !== null && ( <Marker position={[this.state.ownLat, this.state.ownLng]}> @@ -180,6 +182,7 @@ class UserMap extends Component { currentGameId={this.props.currentGameId} socketSignal={this.props.socketSignal} /> + {this.props.children} </Map> ); } 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..56d311958ee2b2c5fdb1f28a5ccc6c5bcce7c51f --- /dev/null +++ b/src/track-playback/src/control.trackplayback/control.playback.js @@ -0,0 +1,484 @@ +import L from "leaflet"; + +export const TrackPlayBackControl = L.Control.extend({ + options: { + position: "topright", + showOptions: true, + showInfo: true, + showSlider: true, + autoPlay: false + }, + + initialize: function(trackplayback, map, options) { + this.map = map; + L.Control.prototype.initialize.call(this, options); + this.trackPlayBack = trackplayback; + this.trackPlayBack.on("tick", this._tickCallback, this); + this.leafletDrawings = []; + }, + + // absolutely disgusting, never use geojson + // init object to pass drawing data to right function + _drawFunction: function(data) { + // create leaflet objects + var createMarker = data => { + data.geometry.coordinates = [ + data.geometry.coordinates[1], + data.geometry.coordinates[0] + ]; + return L.marker(data.geometry.coordinates).addTo(this.map); + }; + var createPolyline = data => { + data.geometry.coordinates = data.geometry.coordinates.map(cords => { + return [cords[1], cords[0]]; + }); + return L.polyline(data.geometry.coordinates, { + color: data.properties.color + }).addTo(this.map); + }; + var createPolygon = data => { + // geoJSON has lat lng wrong way so turn them around + data.geometry.coordinates = data.geometry.coordinates[0].map(cords => { + return [cords[1], cords[0]]; + }); + return L.polygon(data.geometry.coordinates, { + color: data.properties.color + }).addTo(this.map); + }; + var createRectangle = data => { + return L.rectangle(data.geometry.coordinates, { + color: data.properties.color + }).addTo(this.map); + }; + var createCircle = data => { + data.geometry.coordinates = [ + data.geometry.coordinates[1], + data.geometry.coordinates[0] + ]; + return L.circle(data.geometry.coordinates, { + radius: data.properties.radius + }).addTo(this.map); + }; + // handle faulty cords + if (!data.geometry.coordinates[0][0] && !data.geometry.type === "Point") { + return null; + } + if (data.geometry.type === "Point" && !data.properties.radius) { + data.geometry.type = "Marker"; + } + var obj = { + Marker: createMarker, + LineString: createPolyline, + Polygon: createPolygon, + Rectangle: createRectangle, + Point: createCircle + }; + return obj[data.geometry.type](data); + }, + + 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._filterContainer = this._createContainer( + "filterContainer", + this._container + ); + /* this._lineCbx = this._createCheckbox( + "show trackLine", + "show-trackLine", + this._optionsContainer, + this._showTrackLine + ); */ + // create checkboxes for filtering persons based on their class + this._filterInfantry = this._createCheckbox( + "show infantry units", + "show-infantry", + this._filterContainer, + this._showInfantry + ); + this._filterRecon = this._createCheckbox( + "show recon units", + "show-recon", + this._filterContainer, + this._showRecon + ); + this._filterMechanized = this._createCheckbox( + "show mechanized units", + "show-mechanized", + this._filterContainer, + this._showMechanized + ); + // show some text between class based and faction based filtering + this._factionText = this._createInfo( + "Faction filtering:", + "", + "faction-text-filter", + this._filterContainer + ); + // get factions in replay + let factions = this.trackPlayBack.passFactions(); + // create checkboxes for filtering persons based on their faction + this._factionCheckboxes = factions.map(faction => { + return this._createCheckbox( + `show ${faction.name}`, + `show-${faction.name}`, + this._filterContainer, + this._showFaction + ); + }); + // create a div container for score replay + this._scoreContainer = this._createContainer( + "scoreContainer", + this._container + ); + // create score blocks for each faction + this._factionScoreboxes = factions.map(faction => { + return this._createInfo( + `${faction.name}: `, + 0, + "scoreBlock", + this._scoreContainer + ); + }); + + 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( + "decrease speed", + "btn-slow", + this._buttonContainer, + this._slow + ); + this._quickSpeedBtn = this._createButton( + "increase speed", + "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); + inputEle.checked = true; + + 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(); + } + }, + + _showInfantry(e) { + this.trackPlayBack.toggleInfantry(e.target.checked); + }, + + _showRecon(e) { + this.trackPlayBack.toggleRecon(e.target.checked); + }, + + _showMechanized(e) { + this.trackPlayBack.toggleMechanized(e.target.checked); + }, + _showFaction(e) { + this.trackPlayBack.toggleFactions( + e.target.checked, + e.target.parentNode.className.substring( + 5, + e.target.parentNode.className.indexOf(" ") + ) + ); + }, + + _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(); + } + // tick scores + for (let i = 0; i < this._factionScoreboxes.length; i++) { + this._factionScoreboxes[i].innerHTML = this.trackPlayBack.passScores(i); + } + // tick drawings + let drawings = this.trackPlayBack.passDrawings(); + for (let i = 0; i < drawings.length; i++) { + // skip if undefined + if (!drawings[i] && this.leafletDrawings[i]) { + this.map.removeLayer(this.leafletDrawings[i]); + this.leafletDrawings[i] = null; + return; + } + if (!drawings[i]) return; + // remove if it's not active + if (!drawings[i].drawingIsActive && this.leafletDrawings[i]) { + this.map.removeLayer(this.leafletDrawings[i]); + this.leafletDrawings[i] = null; + return; + } + // else draw the marker if it's not drawn + if (drawings[i].drawingIsActive && !this.leafletDrawings[i]) { + this.leafletDrawings[i] = this._drawFunction(drawings[i].data); + console.log(this.leafletDrawings[i]); + } + } + // + // 更新时间轴 + } +}); + +export const trackplaybackcontrol = function(trackplayback, map, options) { + return new TrackPlayBackControl(trackplayback, map, 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..8fdfefcb7a6e9813126ec42f006944ab3854bd53 --- /dev/null +++ b/src/track-playback/src/leaflet.trackplayback/draw.js @@ -0,0 +1,402 @@ +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 + }, + filterOptions: { + infantry: true, + recon: true, + mechanized: true, + factions: [] + }, + + 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); + L.extend(this.filterOptions, options.filterOptions); + L.extend(this.scoreOptions, options.scoreOptions); + + 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]; + // get info from first trackpoint + let info = trackpoints[0].info; + let skip = false; + // check if faction has been filtered and skip drawing if it is + this.filterOptions.factions.forEach(faction => { + if ( + !faction.active && + trackpoints[0].info[1]["value"] === faction.colour + ) { + skip = true; + } + }); + + // compare icon to filter, draw if true else skip + if (!skip && this.filterOptions[info[0]["value"].slice(0, -4)]) { + 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..99596410b3f27d226e8e00e450190fe1bd2968ed --- /dev/null +++ b/src/track-playback/src/leaflet.trackplayback/trackcontroller.js @@ -0,0 +1,97 @@ +import L from "leaflet"; + +import { isArray } from "./util"; +import { Track } from "./track"; + +/** + * 控制器类 + * 控制轨迹和绘制 + */ +export const TrackController = L.Class.extend({ + initialize: function(data, draw, options) { + L.setOptions(this, options); + this._activeScores = []; + this._activeDrawings = []; + this._tracks = data.tracks; + this._scores = data.scores; + this._drawings = data.drawings; + this.addTrack(data.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(); + // draw player locations + 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); + } + // add scores to counter + for (let i = 0; i < this._scores.length; i++) { + let newScore = 0; + let scores = this._scores[i]; + for (let j = 0; j < scores.length; j++) { + if (scores[j].timestamp < time) { + newScore = scores[j].score; + } + } + this._activeScores[i] = newScore; + } + // draw mapdrawings to map + for (let i = 0; i < this._drawings.length; i++) { + let drawing = null; + for (let j = 0; j < this._drawings[i].length; j++) { + if (this._drawings[i][j].timestamp < time) { + drawing = this._drawings[i][j]; + } + } + this._activeDrawings[i] = drawing; + } + }, + + _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..d707967afe0cb3fa5cb76daa9107924f730548d0 --- /dev/null +++ b/src/track-playback/src/leaflet.trackplayback/trackplayback.js @@ -0,0 +1,166 @@ +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, + filterOptions: { factions: data.factions } + }; + this.tracks = this._initTracks(data.players); + this.draw = new Draw(map, drawOptions); + this.trackController = new TrackController( + { + tracks: this.tracks, + scores: data.scores, + drawings: data.drawings + }, + 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; + }, + // toggles the visibility of infantry units on the map + toggleInfantry: function(e) { + this.draw.filterOptions.infantry = e; + return this; + }, + // toggles the visibility of recon units on the map + toggleRecon: function(e) { + this.draw.filterOptions.recon = e; + return this; + }, + // toggles the visibility of mechanized units on the map + toggleMechanized: function(e) { + this.draw.filterOptions.mechanized = e; + return this; + }, + // toggles the visibility of faction units on the map + toggleFactions: function(e, target) { + for (let faction of this.draw.filterOptions.factions) { + if (faction.name === target) { + faction.active = e; + break; + } + } + }, + // pass the factions to control playback to show faction names on the control panel + passFactions: function() { + return this.draw.filterOptions.factions; + }, + // pass current scores to control panel + passScores: function(i) { + return this.trackController._activeScores[i]; + }, + // pass current drawings to control panel + passDrawings: function() { + return this.trackController._activeDrawings; + }, + 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.players, 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]' +}