Categories
discuss

React-Google-Maps displays two markers, one at the original position and another marker that follows the new center of the map onDrag

I am using react-google-maps to show the location of the user after fetching her co-ordinates using ReactJS. But, when I move marker from the original position, the initial marker stays there, so two markers are created. I don’t know how to fix this.

I want that whatever happens, the Map Marker remains at center, also when the user drags OR zoom in/out of the marker, the marker remains at center, so that the user’s location is always at the center of the map. In this way, the user will be able to update her location. As per your example, when I drag, the marker stays fixed in it’s location This component was designed so that the user can set her location, with a little bit of tweaking the Marker’s position, in case the marker is a little bit off Any Help will be appreciated:

App.js

import React from "react";
import WrappedMap from "./Map";
import "./styles.css";

export default class App extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      location: "",
      place: ""
    };
  }

  handleLocation = (location) => {
    this.setState({ location: location });
  };

  handlePlace = (place) => {
    this.setState({ place: place });
  };

  render() {
    return (
      <div className="App">
        <WrappedMap
          googleMapURL={`https://maps.googleapis.com/maps/api/js?key=`}
          loadingElement={<div style={{ height: `100%` }} />}
          containerElement={
            <div
              style={{
                height: `50%`,
                width: "95%",
                position: "absolute",
                marginTop: "25%"
              }}
            />
          }
          mapElement={<div style={{ height: `100%` }} />}
          location={this.state.location}
          handleLocation={this.handleLocation}
          changeState={this.changeState}
          place={this.state.place}
          handlePlace={this.handlePlace}
          handleUseGPS={this.handleUseGPS}
        />
      </div>
    );
  }
}

Map.js

import React, { useRef, useState, useEffect } from "react";
import Geocode from "react-geocode";
import Button from "@material-ui/core/Button";
import {
  GoogleMap,
  withScriptjs,
  withGoogleMap,
  Marker
} from "react-google-maps";
// import "./Sign.css";
function Map({
  location,
  handleLocation,
  changeState,
  place,
  handlePlace,
  handleUseGPS
}) {
  const [center, setCenter] = useState(location);
  const [showMap, setShowMap] = useState(false);
  const refMap = useRef(null);
  var options = {
    enableHighAccuracy: true,
    timeout: 10000,
    maximumAge: 30000
  };
  function success(pos) {
    var crd = pos.coords;
    console.log(crd);
    console.log("Your current position is:");
    console.log(`Latitude : ${crd.latitude}`);
    console.log(`Longitude: ${crd.longitude}`);
    console.log(`More or less ${crd.accuracy} meters.`);
    const loc = {
      lat: crd.latitude,
      lng: crd.longitude
    };
    handleLocation(loc);
    getAndChangeAddress(loc);
    setCenter(loc);
    setShowMap(true);
  }
  function error(err) {
    if (!navigator.geolocation) {
      console.log("Geolocation is not supported by your browser");
    } else {
      console.log("loading");
    }
    let typeErr = err.code;
    console.log(`Code: ${typeErr}`);
    switch (typeErr) {
      case 1:
        console.log("User has not given permissions");
        break;
      case 2:
        console.log(
          "The acquisition of the geolocation failed because at least one internal source of position returned an internal error."
        );
        break;
      case 3:
        console.log("Timeout reached before obtaining information");
        break;
      default:
        break;
    }
    console.warn(`ERROR(${err.code}): ${err.message}`);
    handlePlace("");
    handleLocation({});
    // handleUseGPS(true);
    // changeState(7);
  }
  const handleBoundsChanged = () => {
    const mapCenter = refMap.current.getCenter(); //get map center
    setCenter(mapCenter);
  };
  useEffect(() => {
    navigator.geolocation.getCurrentPosition(success, error, options);
  }, []);
  const handleDragEnd = () => {
    const newCenter = refMap.current.getCenter();
    const newLocation = {
      lat: newCenter.lat(),
      lng: newCenter.lng()
    };
    handleLocation(newLocation);
    getAndChangeAddress(newLocation);
  };
  const returnToMenu = () => {
    // changeState(4);
  };
  const getAndChangeAddress = (loc) => {
    const lat = loc.lat.toString();
    const lng = loc.lng.toString();
    console.log(typeof lat);
    console.log(`From getAddress() function => lat: ${lat},  lng: ${lng}`);
    Geocode.fromLatLng(lat, lng).then(
      (response) => {
        const address = response.results[0].formatted_address;
        console.log(`Formatted address: ${address}`);
        handlePlace(address);
      },
      (error) => {
        console.error(error);
        console.log("Error occuredd in getting address");
      }
    );
  };
  return (
    <>
      <div className="sign-in-form">
        {showMap && (
          <GoogleMap
            ref={refMap}
            defaultZoom={15}
            defaultCenter={center}
            onBoundsChanged={handleBoundsChanged}
            onDragEnd={handleDragEnd}
          >
            <Marker
              // defaultPlace={center}
              position={center}
              // ref={refMap}
              // defaultPosition={center}
              // onDrag={handleBoundsChanged}
              // onDragEnd={handleDragEnd}
            />
          </GoogleMap>
        )}
        {location.lat !== "" && (
          <>
            <hr />
            <div style={{ margin: "1em" }}>{place}</div>
            <hr />
          </>
        )}
        <Button
          className="otp-button"
          onClick={returnToMenu}
          fullWidth
          variant="contained"
        >
          SAVE LOCATION
        </Button>
      </div>
    </>
  );
}
export default withScriptjs(withGoogleMap(Map));

Also See: CodeSandbox Link

Answer

I believe what is happening here is you have set your marker position to center so whenever you drag, a second marker will be generated.

Instead, the react-google-maps docs show an option where you can hard code the latitude and longitude to the marker. Benefits: no duplicate marker. Con: If the user enters a different address that requires the marker to move, you’ll need to write an update function.

Change these lines and your issue should be resolved:

initialize hook

function Map({
  location,
  handleLocation,
  changeState,
  place,
  handlePlace,
  handleUseGPS
}) {
  const [center, setCenter] = useState(location);
  const [showMap, setShowMap] = useState(false);
  const [mylat, setLat] = useState(0);              {/* <------ add this hook */}
  const [mylong, setLong] = useState(0);            {/* <------ and this hook */}
  const refMap = useRef(null);

...

function success()

  function success(pos) {
    var crd = pos.coords;
    console.log(crd);
    console.log("Your current position is:");
    console.log(`Latitude : ${crd.latitude}`);
    console.log(`Longitude: ${crd.longitude}`);
    console.log(`More or less ${crd.accuracy} meters.`);

    setLat(crd.latitude);                   {/* <------ set state here*/}
    setLong(crd.longitude);                 {/* <------ set state here*/}

    const loc = {
      lat: crd.latitude,
      lng: crd.longitude
    };
    handleLocation(loc);
    getAndChangeAddress(loc);
    setCenter(loc);
    setShowMap(true);
  }

return google map

          <GoogleMap
            ref={refMap}
            defaultZoom={15}
            defaultCenter={center}
            onBoundsChanged={handleBoundsChanged}
            onDragEnd={handleDragEnd}
          >
            <Marker
              // defaultPlace={center}
              position={{ lat: mylat, lng: mylong}}        {/* <----- lat and long here */}
              // ref={refMap}
              // defaultPosition={center}
              // onDrag={handleBoundsChanged}
              // onDragEnd={handleDragEnd}
            />
          </GoogleMap>

OP responded with a clarification that <Marker /> should actually move with the center of the screen, and the issue is that there is a duplicate.

After much debugging, I found the error is due to the way the element is rendered. Change:

index.js

import React, { Component } from 'react';
import { render } from 'react-dom';

import App from "./App";

render(<App />, document.getElementById('root'));

And your issue is resolved. This also works:

import React from "react";
import ReactDOM from "react-dom";

import App from "./App";

const rootElement = document.getElementById("root");
ReactDOM.render(
    <App />, {/* <-------- remove <StrictMode /> */}
  rootElement
);

There is a warning in your console about strict mode, and turning it off seems to fix your issue and is the root of the reason why your GoogleMap component was not working as intended:

Warning: Legacy context API has been detected within a strict-mode tree. The old API will be supported in all 16.x releases, but applications using it >should migrate to the new version. Please update the following components: GoogleMap, Marker Learn more about this warning here: … in StrictMode (at src/index.js:8)


I also found another StackOverflow question that your code was modeled from, so I am going to link here for future viewers as well:

Categories
discuss

Blazor wasm invoke javascript, pass large array is very slow

I have a blazor wasm app. In that I am invoking a javascript function that receives an array of double. This is very slow, especially when the array is large.

For a test see the code below:

javascript (“test.js”):

function testSumArray(array) {
    var t0 = performance.now();
    sumArray(array);
    var t1 = performance.now();
    console.log('From JS, time to sum: ' + (t1 - t0) / 1000 + ' s');
}

function sumArray(array) {
    var i;
    var s = 0;
    for (i = 0; i < array.length; i++) {
        s += array[i];
    }
    return s;
}

And c# code (index.razor):

@page "/"
@inject IJSRuntime JSRuntime;

@using System.Text
@using BlazorWasmOnlyTest.Shared
<h1>Hello, world!</h1>

Welcome to your new app.

<div class="container">
    <div class="row mb-2">
        <div class="col">
            <button class="btn btn-primary" @onclick="@TestInvokeJS">Test invoke js</button>
        </div>
    </div>
</div>

@code {
    private int _id;
    private string _status = "";
    private DataInputFileForm _dataInputFileForm;

    private async void TestInvokeJS()
    {
        var n = 100000;
        var array = new double[n];
        for (int i = 0; i < n; i++)
        {
            array[i] = i;
        }
        var w = new System.Diagnostics.Stopwatch();
        w.Start();
        await JSRuntime.InvokeVoidAsync("testSumArray",array);
        w.Stop();
        Console.WriteLine($"C# time to invoke js and sum: {w.ElapsedMilliseconds/1000:F3} s");
    }
}

And for completion – index.html:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <title>BlazorWasmOnlyTest</title>
    <base href="/" />
    <link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
    <link href="css/app.css" rel="stylesheet" />
    <script src="js/test.js"></script>
</head>

<body>
    <app>Loading...</app>

    <div id="blazor-error-ui">
        An unhandled error has occurred.
        <a href="" class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    </div>
    <script src="_framework/blazor.webassembly.js"></script>
</body>

</html>

Running this once gives the following output on my machine:

From JS, time to sum: 0.0037800000282004476 s

C# time to invoke js and sum: 7.000 s

That seems like a pretty high overhead time… Does anyone know if there is a way to speed this up (the real function does something I presently cannot do in Blazor/C# – updating a layer in Leaflet)

EDIT: I have tried the synchronous method described here, without any difference in execution time.

c#:

    var jsInProcess2 = (IJSInProcessRuntime)JSRuntime;
    jsInProcess2.InvokeVoid("testSumArray", array);

js: javascript same as testSumArray above.

EDIT 2:

I have tried passing a JSON string with synchronous interop:

c#:

    var jsInProcess3 = (IJSInProcessRuntime)JSRuntime;
    var array_json3 = System.Text.Json.JsonSerializer.Serialize(array);
    jsInProcess3.InvokeVoid("testSumArray3", array_json);

js:

function testSumArray3(array_json_string) {
    var t0 = performance.now();
    var array = JSON.parse(array_json_string);
    var s = sumArray(array);
    var t1 = performance.now();
    console.log('From JS, time to sum: ' + (t1 - t0) / 1000 + ' s');
    console.log('Array sum = ' + s);
}

and with JSON string and InvokeUnmarshalled js interopcall:

c#:

    var jsInProcess4 = (Microsoft.JSInterop.WebAssembly.WebAssemblyJSRuntime)JSRuntime;
    var array_json4 = System.Text.Json.JsonSerializer.Serialize(array);
    jsInProcess4.InvokeUnmarshalled<string,string>("testSumArray4", array_json4);

js:

function testSumArray4(array_mono_json_string) {
    var t0 = performance.now();
    const array_json_string = BINDING.conv_string(array_mono_json_string);
    var array = JSON.parse(array_json_string);
    var s = sumArray(array);
    var t1 = performance.now();
    console.log('From JS, time to sum: ' + (t1 - t0) / 1000 + ' s');
    console.log('Array sum = ' + s);
}

All methods take approximately the same time, 6-7 secs to complete (of that about 0.0015-0.006 seconds in the javascript function).

I have tried to figure out how to call unmarshalled passing an array, using BINDING.mono_array_to_js_array found in in this file but that throws a long error. c#:

    var sum = jsInProcess4.InvokeUnmarshalled<double[],double>("testSumArray5",array)

js:

function testSumArray5(array_in) {
    var t0 = performance.now();
    var array = BINDING.mono_array_to_js_array(array_in);
    console.log(array[0]);
    var s = sumArray(array);
    var t1 = performance.now();
    console.log('From JS, time to sum: ' + (t1 - t0) / 1000 + ' s');
    console.log('Array sum = ' + s);
    return s;
}

Answer

Just found the way to use .net byte or float arrays in js.

c#:

[Inject] //Injected JSRuntime from Blazor DI
private IJSRuntime JSRuntime { get; set; }

byte[] bytes1;
float[] floats2;
...
if (JSRuntime is IJSUnmarshalledRuntime webAssemblyJSRuntime)
{
    webAssemblyJSRuntime.InvokeUnmarshalled<byte[], float[], object> 
        ("downloadArray", bytes1, floats2);
}

JavaScript:

function downloadArray(bytes1, floats2) {
    // Easy way to convert Uint8 arrays
    var byteArray = Blazor.platform.toUint8Array(bytes1);

    // Adapted method above for float32
    var m = floats2 + 12;
    var r = Module.HEAP32[m >> 2]
    var j = new Float32Array(Module.HEAPF32.buffer, m + 4, r);
}

Here result is Uint8Array and Float32Array objects from byte[] and float[] respectively within a reasonable period of time.

May be there are any approaches to get js arrays because you have access to the whole .net heap from ArrayBuffers like Module.HEAPU8 (heap inside Uint8Array) or Module.HEAPF32 (heap inside Float32Array) and can easily access objects by pointer from InvokeUnmarshalled parameters.

Categories
discuss

Why does @Transactional isolation level have no effect when updating entities with Spring Data JPA?

For this experimental project based on the spring-boot-starter-data-jpa dependency and H2 in-memory database, I defined a User entity with two fields (id and firstName) and declared a UsersRepository by extending the CrudRepository interface.

Now, consider a simple controller which provides two endpoints: /print-user reads the same user twice with some interval printing out its first name, and /update-user is used to change the user’s first name in between those two reads. Notice that I deliberately set Isolation.READ_COMMITTED level and expected that during the course of the first transaction, a user which is retrieved twice by the same id will have different names. But instead, the first transaction prints out the same value twice. To make it more clear, this is the complete sequence of actions:

  1. Initially, jeremy‘s first name is set to Jeremy.
  2. Then I call /print-user which prints out Jeremy and goes to sleep.
  3. Next, I call /update-user from another session and it changes jeremy‘s first name to Bob.
  4. Finally, when the first transaction gets awakened after sleep and re-reads the jeremy user, it prints out Jeremy again as his first name even though the first name has already been changed to Bob (and if we open the database console, it’s now indeed stored as Bob, not Jeremy).

It seems like setting isolation level has no effect here and I’m curious why this is so.

@RestController
@RequestMapping
public class UsersController {

    private final UsersRepository usersRepository;

    @Autowired
    public UsersController(UsersRepository usersRepository) {
        this.usersRepository = usersRepository;
    }

    @GetMapping("/print-user")
    @ResponseStatus(HttpStatus.OK)
    @Transactional (isolation = Isolation.READ_COMMITTED)
    public void printName() throws InterruptedException {
        User user1 = usersRepository.findById("jeremy"); 
        System.out.println(user1.getFirstName());
        
        // allow changing user's name from another 
        // session by calling /update-user endpoint
        Thread.sleep(5000);
        
        User user2 = usersRepository.findById("jeremy");
        System.out.println(user2.getFirstName());
    }


    @GetMapping("/update-user")
    @ResponseStatus(HttpStatus.OK)
    @Transactional(isolation = Isolation.READ_COMMITTED)
    public User changeName() {
        User user = usersRepository.findById("jeremy"); 
        user.setFirstName("Bob");
        return user;
    }
    
}

Answer

There are two issues with your code.

You are performing usersRepository.findById("jeremy"); twice in the same transaction, chances are your second read is retrieving the record from the Cache. You need to refresh the cache when you read the record for the second time. I have updated code which uses entityManager, please check how it can be done using the JpaRepository

User user1 = usersRepository.findById("jeremy"); 
Thread.sleep(5000);
entityManager.refresh(user1);    
User user2 = usersRepository.findById("jeremy");

Here are the logs from my test case, please check SQL queries:

  • The first read operation is completed. Thread is waiting for the timeout.

Hibernate: select person0_.id as id1_0_0_, person0_.city as city2_0_0_, person0_.name as name3_0_0_ from person person0_ where person0_.id=?

  • Triggered update to Bob, it selects and then updates the record.

Hibernate: select person0_.id as id1_0_0_, person0_.city as city2_0_0_, person0_.name as name3_0_0_ from person person0_ where person0_.id=?

Hibernate: update person set city=?, name=? where id=?

  • Now thread wakes up from Sleep and triggers the second read. I could not see any DB query triggered i.e the second read is coming from the cache.

The second possible issue is with /update-user endpoint handler logic. You are changing the name of the user but not persisting it back, merely calling the setter method won’t update the database. Hence when other endpoint’s Thread wakes up it prints Jeremy.

Thus you need to call userRepository.saveAndFlush(user) after changing the name.

@GetMapping("/update-user")
@ResponseStatus(HttpStatus.OK)
@Transactional(isolation = Isolation.READ_COMMITTED)
public User changeName() {
    User user = usersRepository.findById("jeremy"); 
    user.setFirstName("Bob");
    userRepository.saveAndFlush(user); // call saveAndFlush
    return user;
}

Also, you need to check whether the database supports the required isolation level. You can refer H2 Transaction Isolation Levels

Source: stackoverflow
Text is available under the Creative Commons Attribution-ShareAlike License; additional terms may apply. By using this site, you agree to the Privacy Policy, and Copyright Policy. Content is available under CC BY-SA 3.0 unless otherwise noted. The answers/resolutions are collected from stackoverflow, are licensed under cc by-sa 2.5 , cc by-sa 3.0 and cc by-sa 4.0 © No Copyrights, All Questions are retrived from public domain..