You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

312 lines
6.8 KiB

  1. package react.map;
  2. import js.Promise;
  3. import react.ReactComponent;
  4. import react.ReactMacro.jsx;
  5. import utils.HttpUtil;
  6. import leaflet.L;
  7. import Common;
  8. using Lambda;
  9. @:jsRequire('react-places-autocomplete', 'default')
  10. extern class Autocomplete extends ReactComponent {}
  11. @:jsRequire('react-places-autocomplete')
  12. extern class GeoUtil {
  13. static function geocodeByAddress(address:Dynamic):Promise<Dynamic>;
  14. }
  15. @:jsRequire('geolib')
  16. extern class Geolib {
  17. static function getDistance(start:Dynamic, end:Dynamic):Float;
  18. }
  19. /**
  20. * Groups Map
  21. * @author rcrestey
  22. */
  23. typedef GroupMapRootState = {
  24. var point:Dynamic;
  25. var address:String;
  26. var groups:Array<GroupOnMap>;
  27. var groupFocusedId:Int;
  28. var isInit:Bool;
  29. };
  30. typedef GroupMapRootProps = {
  31. var lat:Float;
  32. var lng:Float;
  33. var address:String;
  34. };
  35. class GroupMapRoot extends ReactComponentOfState<GroupMapRootState>{
  36. static inline var GROUP_MAP_URL = '/api/group/map';
  37. var distanceMap = new Map<Int,Dynamic>();
  38. public function new(props)
  39. {
  40. super(props);
  41. state = {
  42. point: L.latLng(props.lat, props.lng),
  43. address: props.address,
  44. groups: [],
  45. groupFocusedId: null,
  46. isInit: false
  47. };
  48. }
  49. function onChange(address) {
  50. setState({
  51. address: address
  52. });
  53. }
  54. function openPopup(group:Dynamic) {
  55. setState({
  56. groupFocusedId: group.place.id
  57. });
  58. }
  59. function closePopup() {
  60. setState({
  61. groupFocusedId: null
  62. });
  63. }
  64. function geocodeByAddress(address:String):Promise<Dynamic> {
  65. return GeoUtil.geocodeByAddress(address)
  66. .then(function(results) {
  67. var lat = results[0].geometry.location.lat();
  68. var lng = results[0].geometry.location.lng();
  69. return {lat: lat, lng: lng};
  70. });
  71. }
  72. /**
  73. * Call API to find groups at $lat and $lng
  74. * @param lat -
  75. * @param lng -
  76. */
  77. function fetchGroups(lat:Float, lng:Float) {
  78. HttpUtil.fetch(GROUP_MAP_URL, GET, {lat: lat, lng: lng}, JSON)
  79. .then(function(results) {
  80. setState({
  81. point: L.latLng(lat, lng),
  82. groups: results.groups,
  83. isInit: true
  84. }, fillDistanceMap);
  85. })
  86. .catchError(function(error) {
  87. trace('Error', error);
  88. });
  89. }
  90. /**
  91. * Call API to look for groups in the defined bounding box
  92. */
  93. var wait : Bool;
  94. function fetchGroupsInsideBox(newBox) {
  95. switch(wait){
  96. case null,false : wait = true;
  97. case true : trace("stop"); return;
  98. }
  99. HttpUtil.fetch(GROUP_MAP_URL, GET, newBox, JSON)
  100. .then(function(results) {
  101. wait = false;
  102. setState({
  103. groups: results.groups
  104. }, fillDistanceMap);
  105. });
  106. /*.catchError(function(error) {
  107. trace('Error', error + " stack:"+haxe.CallStack.toString(haxe.CallStack.exceptionStack())) ;
  108. wait = false;
  109. });*/
  110. }
  111. function getGroupDistance(group:GroupOnMap):Float {
  112. if (state.point == null)
  113. return null;
  114. var start = {
  115. latitude: state.point.lat,
  116. longitude: state.point.lng
  117. };
  118. var end = {
  119. latitude: group.place.latitude,
  120. longitude: group.place.longitude
  121. };
  122. return Geolib.getDistance(start, end);
  123. }
  124. function fillDistanceMap() {
  125. for (group in state.groups) {
  126. distanceMap.set(group.place.id, getGroupDistance(group));
  127. }
  128. orderGroupsByDistance(state.groups);
  129. setState({
  130. groups: state.groups
  131. });
  132. }
  133. function orderGroupsByDistance(groups:Array<GroupOnMap>) {
  134. groups.sort(function(a, b) {
  135. return distanceMap.get(a.place.id) - distanceMap.get(b.place.id);
  136. });
  137. }
  138. function convertDistance(distance:Int):String { // to test
  139. if (distance > 10000)
  140. return Math.floor(distance / 1000) + ' km';
  141. if (distance > 1000)
  142. return Math.floor(distance / 100) / 10 + ' km';
  143. return distance + ' m';
  144. }
  145. function handleSelect(address:String) {
  146. setState({
  147. address: address
  148. });
  149. geocodeByAddress(address)
  150. .then(function(coord) {
  151. fetchGroups(coord.lat, coord.lng);
  152. })
  153. .catchError(function(error) {
  154. trace('Error', error);
  155. });
  156. }
  157. override public function componentDidMount() {
  158. if (state.point != null)
  159. fetchGroups(state.point.lat, state.point.lng);
  160. else if (state.address != '')
  161. handleSelect(state.address);
  162. }
  163. function renderSuggestion(obj:Dynamic) {
  164. return jsx('
  165. <div className="autocomplete-item">
  166. <i className="fa fa-map-marker autocomplete-icon" />
  167. <strong>${obj.formattedSuggestion.mainText}</strong>&nbsp;
  168. <small className="text-muted">${obj.formattedSuggestion.secondaryText}</small>
  169. </div>
  170. ');
  171. }
  172. override public function render() {
  173. var inputProps = {
  174. value: state.address,
  175. onChange: onChange
  176. };
  177. var cssClasses = {
  178. root: 'form-group',
  179. input: 'autocomplete-input',
  180. autocompleteContainer: 'autocomplete-results',
  181. };
  182. return jsx('
  183. <div className="group-map">
  184. <div className="row">
  185. <div id="logo" className="col-md-3">&nbsp;</div>
  186. <div className="col-md-9">
  187. <div className="form-group-container">
  188. Trouvez un groupe Cagette près de chez vous &nbsp;
  189. <Autocomplete
  190. inputProps=${inputProps}
  191. onSelect=${handleSelect}
  192. classNames=${cssClasses}
  193. renderSuggestion=${renderSuggestion}
  194. placeHolder="Saisissez votre adresse"
  195. />
  196. </div>
  197. </div>
  198. </div>
  199. <div className="row">
  200. <div className="col-md-3" id="groupsContainer">${renderGroupList()}</div>
  201. <div className="col-md-9" id="mapContainer">${renderGroupMap()}</div>
  202. </div>
  203. </div>');
  204. }
  205. function renderGroupMap() {
  206. if (!state.isInit)
  207. return null;
  208. return jsx('
  209. <GroupMap
  210. addressCoord=${state.point}
  211. groups=${state.groups}
  212. fetchGroupsInsideBox=${fetchGroupsInsideBox}
  213. groupFocusedId=${state.groupFocusedId}
  214. />
  215. ');
  216. }
  217. function renderGroupList() {
  218. var groups = state.groups.map(function(group) {
  219. return renderGroup(group);
  220. });
  221. return jsx('
  222. <div className="groups">
  223. ${groups}
  224. </div>
  225. ');
  226. }
  227. /**
  228. * Renders a group in the left list
  229. */
  230. function renderGroup(group:GroupOnMap) {
  231. var address = [
  232. group.place.address1,
  233. group.place.address2,
  234. [group.place.zipCode, group.place.city].join(" "),
  235. ];
  236. var addressBlock = Lambda.array(address.mapi(function(index, element) {
  237. if (element != null){
  238. return jsx('<div key=${index}>$element</div>');
  239. }else{
  240. return null;
  241. }
  242. }));
  243. var distance = null;
  244. if (distanceMap.get(group.place.id) != null)
  245. distance = jsx('<div className="distance">${convertDistance(distanceMap.get(group.place.id))}</div>');
  246. var classNames = ['clickable groupBlock'];
  247. if (group.place.id == state.groupFocusedId)
  248. classNames.push('focused');
  249. var img = if(group.image==null) {
  250. null;
  251. }else{
  252. jsx('<img src="${group.image}" className="img-responsive" />');
  253. }
  254. return jsx('<a target="_blank"
  255. onMouseEnter=${function() { openPopup(group); }}
  256. onMouseLeave=${closePopup}
  257. className=${classNames.join(' ')}
  258. key=${group.place.id}
  259. href=${"/group/"+group.id}
  260. >
  261. $img
  262. <h4>${group.name}</h4>
  263. <div className="address">${addressBlock}</div>
  264. ${distance}
  265. </a>');
  266. }
  267. }